]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-134745: Change PyThread_allocate_lock() implementation to PyMutex (#134747)
authorVictor Stinner <vstinner@python.org>
Fri, 30 May 2025 10:15:47 +0000 (12:15 +0200)
committerGitHub <noreply@github.com>
Fri, 30 May 2025 10:15:47 +0000 (10:15 +0000)
Co-authored-by: Sam Gross <colesbury@gmail.com>
Include/internal/pycore_lock.h
Lib/test/test_sys.py
Misc/NEWS.d/next/C_API/2025-05-30-11-33-17.gh-issue-134745.GN-zk2.rst [new file with mode: 0644]
Python/lock.c
Python/thread.c
Python/thread_nt.h
Python/thread_pthread.h

index 7484b05d7f2446a00a7d6a1270cba0aeb60eb8d5..32b60cc33a21f1f253c7876d3fb63fae16f86202 100644 (file)
@@ -48,6 +48,9 @@ typedef enum _PyLockFlags {
 
     // Handle signals if interrupted while waiting on the lock.
     _PY_LOCK_HANDLE_SIGNALS = 2,
+
+    // Fail if interrupted by a signal while waiting on the lock.
+    _PY_FAIL_IF_INTERRUPTED = 4,
 } _PyLockFlags;
 
 // Lock a mutex with an optional timeout and additional options. See
index 795d1ecbb59f8f723422244de497db00a1528263..65d15610ed15051f49391061192d3a48d6842fd8 100644 (file)
@@ -729,7 +729,7 @@ class SysModuleTest(unittest.TestCase):
         info = sys.thread_info
         self.assertEqual(len(info), 3)
         self.assertIn(info.name, ('nt', 'pthread', 'pthread-stubs', 'solaris', None))
-        self.assertIn(info.lock, ('semaphore', 'mutex+cond', None))
+        self.assertIn(info.lock, ('pymutex', None))
         if sys.platform.startswith(("linux", "android", "freebsd")):
             self.assertEqual(info.name, "pthread")
         elif sys.platform == "win32":
diff --git a/Misc/NEWS.d/next/C_API/2025-05-30-11-33-17.gh-issue-134745.GN-zk2.rst b/Misc/NEWS.d/next/C_API/2025-05-30-11-33-17.gh-issue-134745.GN-zk2.rst
new file mode 100644 (file)
index 0000000..a85d2e9
--- /dev/null
@@ -0,0 +1,3 @@
+Change :c:func:`!PyThread_allocate_lock` implementation to ``PyMutex``.
+On Windows, :c:func:`!PyThread_acquire_lock_timed` now supports the *intr_flag*
+parameter: it can be interrupted. Patch by Victor Stinner.
index 28a12ad18352d1e1fac0d088876075993bda0701..b125ad0c9e356d50ef0fd219912f279bacc83179 100644 (file)
@@ -119,6 +119,9 @@ _PyMutex_LockTimed(PyMutex *m, PyTime_t timeout, _PyLockFlags flags)
                 return PY_LOCK_INTR;
             }
         }
+        else if (ret == Py_PARK_INTR && (flags & _PY_FAIL_IF_INTERRUPTED)) {
+            return PY_LOCK_INTR;
+        }
         else if (ret == Py_PARK_TIMEOUT) {
             assert(timeout >= 0);
             return PY_LOCK_FAILURE;
index 4ff5f11a34852b2cffa48e2d667bcbcf25c1ff3d..18c4af7f634c75b1e646c8b6053f93fa9f21174a 100644 (file)
@@ -39,7 +39,8 @@
 const long long PY_TIMEOUT_MAX = PY_TIMEOUT_MAX_VALUE;
 
 
-static void PyThread__init_thread(void); /* Forward */
+/* Forward declaration */
+static void PyThread__init_thread(void);
 
 #define initialized _PyRuntime.threads.initialized
 
@@ -71,6 +72,79 @@ PyThread_init_thread(void)
 #endif
 
 
+/*
+ * Lock support.
+ */
+
+PyThread_type_lock
+PyThread_allocate_lock(void)
+{
+    if (!initialized) {
+        PyThread_init_thread();
+    }
+
+    PyMutex *lock = (PyMutex *)PyMem_RawMalloc(sizeof(PyMutex));
+    if (lock) {
+        *lock = (PyMutex){0};
+    }
+
+    return (PyThread_type_lock)lock;
+}
+
+void
+PyThread_free_lock(PyThread_type_lock lock)
+{
+    PyMem_RawFree(lock);
+}
+
+PyLockStatus
+PyThread_acquire_lock_timed(PyThread_type_lock lock, PY_TIMEOUT_T microseconds,
+                            int intr_flag)
+{
+    PyTime_t timeout;  // relative timeout
+    if (microseconds >= 0) {
+        // bpo-41710: PyThread_acquire_lock_timed() cannot report timeout
+        // overflow to the caller, so clamp the timeout to
+        // [PyTime_MIN, PyTime_MAX].
+        //
+        // PyTime_MAX nanoseconds is around 292.3 years.
+        //
+        // _thread.Lock.acquire() and _thread.RLock.acquire() raise an
+        // OverflowError if microseconds is greater than PY_TIMEOUT_MAX.
+        timeout = _PyTime_FromMicrosecondsClamp(microseconds);
+    }
+    else {
+        timeout = -1;
+    }
+
+    _PyLockFlags flags = _Py_LOCK_DONT_DETACH;
+    if (intr_flag) {
+        flags |= _PY_FAIL_IF_INTERRUPTED;
+    }
+
+    return _PyMutex_LockTimed((PyMutex *)lock, timeout, flags);
+}
+
+void
+PyThread_release_lock(PyThread_type_lock lock)
+{
+    PyMutex_Unlock((PyMutex *)lock);
+}
+
+int
+_PyThread_at_fork_reinit(PyThread_type_lock *lock)
+{
+    _PyMutex_at_fork_reinit((PyMutex *)lock);
+    return 0;
+}
+
+int
+PyThread_acquire_lock(PyThread_type_lock lock, int waitflag)
+{
+    return PyThread_acquire_lock_timed(lock, waitflag ? -1 : 0, /*intr_flag=*/0);
+}
+
+
 /* return the current thread stack size */
 size_t
 PyThread_get_stacksize(void)
@@ -261,11 +335,7 @@ PyThread_GetInfo(void)
 #ifdef HAVE_PTHREAD_STUBS
     value = Py_NewRef(Py_None);
 #elif defined(_POSIX_THREADS)
-#ifdef USE_SEMAPHORES
-    value = PyUnicode_FromString("semaphore");
-#else
-    value = PyUnicode_FromString("mutex+cond");
-#endif
+    value = PyUnicode_FromString("pymutex");
     if (value == NULL) {
         Py_DECREF(threadinfo);
         return NULL;
index e078b98be3cdf43fad71d5b641cc19b2a823be1f..9a29d14ef67678d2a8957126f5a5e6bdc94369d6 100644 (file)
@@ -300,98 +300,6 @@ PyThread_hang_thread(void)
     }
 }
 
-/*
- * Lock support. It has to be implemented as semaphores.
- * I [Dag] tried to implement it with mutex but I could find a way to
- * tell whether a thread already own the lock or not.
- */
-PyThread_type_lock
-PyThread_allocate_lock(void)
-{
-    PNRMUTEX mutex;
-
-    if (!initialized)
-        PyThread_init_thread();
-
-    mutex = AllocNonRecursiveMutex() ;
-
-    PyThread_type_lock aLock = (PyThread_type_lock) mutex;
-    assert(aLock);
-
-    return aLock;
-}
-
-void
-PyThread_free_lock(PyThread_type_lock aLock)
-{
-    FreeNonRecursiveMutex(aLock) ;
-}
-
-// WaitForSingleObject() accepts timeout in milliseconds in the range
-// [0; 0xFFFFFFFE] (DWORD type). INFINITE value (0xFFFFFFFF) means no
-// timeout. 0xFFFFFFFE milliseconds is around 49.7 days.
-const DWORD TIMEOUT_MS_MAX = 0xFFFFFFFE;
-
-/*
- * Return 1 on success if the lock was acquired
- *
- * and 0 if the lock was not acquired. This means a 0 is returned
- * if the lock has already been acquired by this thread!
- */
-PyLockStatus
-PyThread_acquire_lock_timed(PyThread_type_lock aLock,
-                            PY_TIMEOUT_T microseconds, int intr_flag)
-{
-    assert(aLock);
-
-    /* Fow now, intr_flag does nothing on Windows, and lock acquires are
-     * uninterruptible.  */
-    PyLockStatus success;
-    PY_TIMEOUT_T milliseconds;
-
-    if (microseconds >= 0) {
-        milliseconds = microseconds / 1000;
-        // Round milliseconds away from zero
-        if (microseconds % 1000 > 0) {
-            milliseconds++;
-        }
-        if (milliseconds > (PY_TIMEOUT_T)TIMEOUT_MS_MAX) {
-            // bpo-41710: PyThread_acquire_lock_timed() cannot report timeout
-            // overflow to the caller, so clamp the timeout to
-            // [0, TIMEOUT_MS_MAX] milliseconds.
-            //
-            // _thread.Lock.acquire() and _thread.RLock.acquire() raise an
-            // OverflowError if microseconds is greater than PY_TIMEOUT_MAX.
-            milliseconds = TIMEOUT_MS_MAX;
-        }
-        assert(milliseconds != INFINITE);
-    }
-    else {
-        milliseconds = INFINITE;
-    }
-
-    if (EnterNonRecursiveMutex((PNRMUTEX)aLock,
-                               (DWORD)milliseconds) == WAIT_OBJECT_0) {
-        success = PY_LOCK_ACQUIRED;
-    }
-    else {
-        success = PY_LOCK_FAILURE;
-    }
-
-    return success;
-}
-int
-PyThread_acquire_lock(PyThread_type_lock aLock, int waitflag)
-{
-    return PyThread_acquire_lock_timed(aLock, waitflag ? -1 : 0, 0);
-}
-
-void
-PyThread_release_lock(PyThread_type_lock aLock)
-{
-    assert(aLock);
-    (void)LeaveNonRecursiveMutex((PNRMUTEX) aLock);
-}
 
 /* minimum/maximum thread stack sizes supported */
 #define THREAD_MIN_STACKSIZE    0x8000          /* 32 KiB */
index da4058242448f3eef79b48d6edf3f23a478bdb6d..13992f95723866b2ea9b24b4930ceb7766c4fff7 100644 (file)
 #undef HAVE_SEM_CLOCKWAIT
 #endif
 
-/* Whether or not to use semaphores directly rather than emulating them with
- * mutexes and condition variables:
- */
-#if (defined(_POSIX_SEMAPHORES) && !defined(HAVE_BROKEN_POSIX_SEMAPHORES) && \
-     (defined(HAVE_SEM_TIMEDWAIT) || defined(HAVE_SEM_CLOCKWAIT)))
-#  define USE_SEMAPHORES
-#else
-#  undef USE_SEMAPHORES
-#endif
-
 
 /* On platforms that don't use standard POSIX threads pthread_sigmask()
  * isn't present.  DEC threads uses sigprocmask() instead as do most
@@ -442,388 +432,6 @@ PyThread_hang_thread(void)
     }
 }
 
-#ifdef USE_SEMAPHORES
-
-/*
- * Lock support.
- */
-
-PyThread_type_lock
-PyThread_allocate_lock(void)
-{
-    sem_t *lock;
-    int status, error = 0;
-
-    if (!initialized)
-        PyThread_init_thread();
-
-    lock = (sem_t *)PyMem_RawMalloc(sizeof(sem_t));
-
-    if (lock) {
-        status = sem_init(lock,0,1);
-        CHECK_STATUS("sem_init");
-
-        if (error) {
-            PyMem_RawFree((void *)lock);
-            lock = NULL;
-        }
-    }
-
-    return (PyThread_type_lock)lock;
-}
-
-void
-PyThread_free_lock(PyThread_type_lock lock)
-{
-    sem_t *thelock = (sem_t *)lock;
-    int status, error = 0;
-
-    (void) error; /* silence unused-but-set-variable warning */
-
-    if (!thelock)
-        return;
-
-    status = sem_destroy(thelock);
-    CHECK_STATUS("sem_destroy");
-
-    PyMem_RawFree((void *)thelock);
-}
-
-/*
- * As of February 2002, Cygwin thread implementations mistakenly report error
- * codes in the return value of the sem_ calls (like the pthread_ functions).
- * Correct implementations return -1 and put the code in errno. This supports
- * either.
- */
-static int
-fix_status(int status)
-{
-    return (status == -1) ? errno : status;
-}
-
-PyLockStatus
-PyThread_acquire_lock_timed(PyThread_type_lock lock, PY_TIMEOUT_T microseconds,
-                            int intr_flag)
-{
-    PyLockStatus success;
-    sem_t *thelock = (sem_t *)lock;
-    int status, error = 0;
-
-    (void) error; /* silence unused-but-set-variable warning */
-
-    PyTime_t timeout;  // relative timeout
-    if (microseconds >= 0) {
-        // bpo-41710: PyThread_acquire_lock_timed() cannot report timeout
-        // overflow to the caller, so clamp the timeout to
-        // [PyTime_MIN, PyTime_MAX].
-        //
-        // PyTime_MAX nanoseconds is around 292.3 years.
-        //
-        // _thread.Lock.acquire() and _thread.RLock.acquire() raise an
-        // OverflowError if microseconds is greater than PY_TIMEOUT_MAX.
-        timeout = _PyTime_FromMicrosecondsClamp(microseconds);
-    }
-    else {
-        timeout = -1;
-    }
-
-#ifdef HAVE_SEM_CLOCKWAIT
-    struct timespec abs_timeout;
-    // Local scope for deadline
-    {
-        PyTime_t now;
-        // silently ignore error: cannot report error to the caller
-        (void)PyTime_MonotonicRaw(&now);
-        PyTime_t deadline = _PyTime_Add(now, timeout);
-        _PyTime_AsTimespec_clamp(deadline, &abs_timeout);
-    }
-#else
-    PyTime_t deadline = 0;
-    if (timeout > 0 && !intr_flag) {
-        deadline = _PyDeadline_Init(timeout);
-    }
-#endif
-
-    while (1) {
-        if (timeout > 0) {
-#ifdef HAVE_SEM_CLOCKWAIT
-            status = fix_status(sem_clockwait(thelock, CLOCK_MONOTONIC,
-                                              &abs_timeout));
-#else
-            PyTime_t now;
-            // silently ignore error: cannot report error to the caller
-            (void)PyTime_TimeRaw(&now);
-            PyTime_t abs_time = _PyTime_Add(now, timeout);
-
-            struct timespec ts;
-            _PyTime_AsTimespec_clamp(abs_time, &ts);
-            status = fix_status(sem_timedwait(thelock, &ts));
-#endif
-        }
-        else if (timeout == 0) {
-            status = fix_status(sem_trywait(thelock));
-        }
-        else {
-            status = fix_status(sem_wait(thelock));
-        }
-
-        /* Retry if interrupted by a signal, unless the caller wants to be
-           notified.  */
-        if (intr_flag || status != EINTR) {
-            break;
-        }
-
-        // sem_clockwait() uses an absolute timeout, there is no need
-        // to recompute the relative timeout.
-#ifndef HAVE_SEM_CLOCKWAIT
-        if (timeout > 0) {
-            /* wait interrupted by a signal (EINTR): recompute the timeout */
-            timeout = _PyDeadline_Get(deadline);
-            if (timeout < 0) {
-                status = ETIMEDOUT;
-                break;
-            }
-        }
-#endif
-    }
-
-    /* Don't check the status if we're stopping because of an interrupt.  */
-    if (!(intr_flag && status == EINTR)) {
-        if (timeout > 0) {
-            if (status != ETIMEDOUT) {
-#ifdef HAVE_SEM_CLOCKWAIT
-                CHECK_STATUS("sem_clockwait");
-#else
-                CHECK_STATUS("sem_timedwait");
-#endif
-            }
-        }
-        else if (timeout == 0) {
-            if (status != EAGAIN) {
-                CHECK_STATUS("sem_trywait");
-            }
-        }
-        else {
-            CHECK_STATUS("sem_wait");
-        }
-    }
-
-    if (status == 0) {
-        success = PY_LOCK_ACQUIRED;
-    } else if (intr_flag && status == EINTR) {
-        success = PY_LOCK_INTR;
-    } else {
-        success = PY_LOCK_FAILURE;
-    }
-
-    return success;
-}
-
-void
-PyThread_release_lock(PyThread_type_lock lock)
-{
-    sem_t *thelock = (sem_t *)lock;
-    int status, error = 0;
-
-    (void) error; /* silence unused-but-set-variable warning */
-
-    status = sem_post(thelock);
-    CHECK_STATUS("sem_post");
-}
-
-#else /* USE_SEMAPHORES */
-
-/*
- * Lock support.
- */
-PyThread_type_lock
-PyThread_allocate_lock(void)
-{
-    pthread_lock *lock;
-    int status, error = 0;
-
-    if (!initialized)
-        PyThread_init_thread();
-
-    lock = (pthread_lock *) PyMem_RawCalloc(1, sizeof(pthread_lock));
-    if (lock) {
-        lock->locked = 0;
-
-        status = pthread_mutex_init(&lock->mut, NULL);
-        CHECK_STATUS_PTHREAD("pthread_mutex_init");
-        /* Mark the pthread mutex underlying a Python mutex as
-           pure happens-before.  We can't simply mark the
-           Python-level mutex as a mutex because it can be
-           acquired and released in different threads, which
-           will cause errors. */
-        _Py_ANNOTATE_PURE_HAPPENS_BEFORE_MUTEX(&lock->mut);
-
-        status = _PyThread_cond_init(&lock->lock_released);
-        CHECK_STATUS_PTHREAD("pthread_cond_init");
-
-        if (error) {
-            PyMem_RawFree((void *)lock);
-            lock = 0;
-        }
-    }
-
-    return (PyThread_type_lock) lock;
-}
-
-void
-PyThread_free_lock(PyThread_type_lock lock)
-{
-    pthread_lock *thelock = (pthread_lock *)lock;
-    int status, error = 0;
-
-    (void) error; /* silence unused-but-set-variable warning */
-
-    /* some pthread-like implementations tie the mutex to the cond
-     * and must have the cond destroyed first.
-     */
-    status = pthread_cond_destroy( &thelock->lock_released );
-    CHECK_STATUS_PTHREAD("pthread_cond_destroy");
-
-    status = pthread_mutex_destroy( &thelock->mut );
-    CHECK_STATUS_PTHREAD("pthread_mutex_destroy");
-
-    PyMem_RawFree((void *)thelock);
-}
-
-PyLockStatus
-PyThread_acquire_lock_timed(PyThread_type_lock lock, PY_TIMEOUT_T microseconds,
-                            int intr_flag)
-{
-    PyLockStatus success = PY_LOCK_FAILURE;
-    pthread_lock *thelock = (pthread_lock *)lock;
-    int status, error = 0;
-
-    if (microseconds == 0) {
-        status = pthread_mutex_trylock( &thelock->mut );
-        if (status != EBUSY) {
-            CHECK_STATUS_PTHREAD("pthread_mutex_trylock[1]");
-        }
-    }
-    else {
-        status = pthread_mutex_lock( &thelock->mut );
-        CHECK_STATUS_PTHREAD("pthread_mutex_lock[1]");
-    }
-    if (status != 0) {
-        goto done;
-    }
-
-    if (thelock->locked == 0) {
-        success = PY_LOCK_ACQUIRED;
-        goto unlock;
-    }
-    if (microseconds == 0) {
-        goto unlock;
-    }
-
-    struct timespec abs_timeout;
-    if (microseconds > 0) {
-        _PyThread_cond_after(microseconds, &abs_timeout);
-    }
-    // Continue trying until we get the lock
-
-    // mut must be locked by me -- part of the condition protocol
-    while (1) {
-        if (microseconds > 0) {
-            status = pthread_cond_timedwait(&thelock->lock_released,
-                                            &thelock->mut, &abs_timeout);
-            if (status == 1) {
-                break;
-            }
-            if (status == ETIMEDOUT) {
-                break;
-            }
-            CHECK_STATUS_PTHREAD("pthread_cond_timedwait");
-        }
-        else {
-            status = pthread_cond_wait(
-                &thelock->lock_released,
-                &thelock->mut);
-            CHECK_STATUS_PTHREAD("pthread_cond_wait");
-        }
-
-        if (intr_flag && status == 0 && thelock->locked) {
-            // We were woken up, but didn't get the lock.  We probably received
-            // a signal.  Return PY_LOCK_INTR to allow the caller to handle
-            // it and retry.
-            success = PY_LOCK_INTR;
-            break;
-        }
-
-        if (status == 0 && !thelock->locked) {
-            success = PY_LOCK_ACQUIRED;
-            break;
-        }
-
-        // Wait got interrupted by a signal: retry
-    }
-
-unlock:
-    if (success == PY_LOCK_ACQUIRED) {
-        thelock->locked = 1;
-    }
-    status = pthread_mutex_unlock( &thelock->mut );
-    CHECK_STATUS_PTHREAD("pthread_mutex_unlock[1]");
-
-done:
-    if (error) {
-        success = PY_LOCK_FAILURE;
-    }
-    return success;
-}
-
-void
-PyThread_release_lock(PyThread_type_lock lock)
-{
-    pthread_lock *thelock = (pthread_lock *)lock;
-    int status, error = 0;
-
-    (void) error; /* silence unused-but-set-variable warning */
-
-    status = pthread_mutex_lock( &thelock->mut );
-    CHECK_STATUS_PTHREAD("pthread_mutex_lock[3]");
-
-    thelock->locked = 0;
-
-    /* wake up someone (anyone, if any) waiting on the lock */
-    status = pthread_cond_signal( &thelock->lock_released );
-    CHECK_STATUS_PTHREAD("pthread_cond_signal");
-
-    status = pthread_mutex_unlock( &thelock->mut );
-    CHECK_STATUS_PTHREAD("pthread_mutex_unlock[3]");
-}
-
-#endif /* USE_SEMAPHORES */
-
-int
-_PyThread_at_fork_reinit(PyThread_type_lock *lock)
-{
-    PyThread_type_lock new_lock = PyThread_allocate_lock();
-    if (new_lock == NULL) {
-        return -1;
-    }
-
-    /* bpo-6721, bpo-40089: The old lock can be in an inconsistent state.
-       fork() can be called in the middle of an operation on the lock done by
-       another thread. So don't call PyThread_free_lock(*lock).
-
-       Leak memory on purpose. Don't release the memory either since the
-       address of a mutex is relevant. Putting two mutexes at the same address
-       can lead to problems. */
-
-    *lock = new_lock;
-    return 0;
-}
-
-int
-PyThread_acquire_lock(PyThread_type_lock lock, int waitflag)
-{
-    return PyThread_acquire_lock_timed(lock, waitflag ? -1 : 0, /*intr_flag=*/0);
-}
 
 /* set the thread stack size.
  * Return 0 if size is valid, -1 if size is invalid,