gh-134745: Change PyThread_allocate_lock() implementation to PyMutex (#134747)

Co-authored-by: Sam Gross <colesbury@gmail.com>
This commit is contained in:
Victor Stinner 2025-05-30 12:15:47 +02:00 committed by GitHub
parent 45c6c48afc
commit ebf6d13567
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 86 additions and 491 deletions

View 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

View 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":

View File

@ -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.

View 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;

View 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;

View 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 */

View File

@ -99,16 +99,6 @@
#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,