blob: 84d7346a758721614220c067fb9c7317d6a18157 [file]
#include "pythoncapi_compat.h"
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "librt_threading.h"
#include "mypyc_util.h"
#if !defined(_WIN32)
#include <unistd.h>
#include <errno.h>
#endif
#if CPY_3_14_FEATURES
// Python 3.14+ (all platforms, with or without the GIL): Use PyMutex (1-byte
// atomic lock with parking lot). PyMutex gives better interruptibility than the
// pthread fallback, and its fast release path is competitive with sem_t.
// PyMutex_LockFast, _PyMutex_LockTimed, and _PY_LOCK_DETACH are internal
// CPython APIs that might change across minor releases.
#define LOCK_BACKEND_PYMUTEX
#ifndef Py_BUILD_CORE
#define Py_BUILD_CORE
#endif
#include "internal/pycore_lock.h"
#elif defined(_WIN32)
// Python <3.14 on Windows: Use Slim Reader/Writer Lock
#define LOCK_BACKEND_SRWLOCK
#include <windows.h>
#else
// Python <3.14 on POSIX.
//
// Prefer a POSIX unnamed semaphore when the platform supports it well and the
// GIL is enabled, and fall back to a pthread mutex + condition variable
// otherwise. We use the same test CPython uses to pick its semaphore-based lock
// (see Python/thread_pthread.h): an unnamed semaphore is only usable when
// sem_init() actually works AND a timed wait is available. Notably this is true
// on Linux but false on macOS (whose sem_init() is a non-functional stub), so
// macOS uses the mutex+condvar fallback. Free-threaded builds also use the
// mutex+condvar fallback because the semaphore backend's `locked` bookkeeping
// relies on GIL serialization.
#if !defined(Py_GIL_DISABLED) && \
defined(_POSIX_SEMAPHORES) && (_POSIX_SEMAPHORES + 0) != -1 && \
(defined(HAVE_SEM_TIMEDWAIT) || defined(HAVE_SEM_CLOCKWAIT))
#define LOCK_BACKEND_SEM
#include <semaphore.h>
#else
#define LOCK_BACKEND_PTHREAD
#include <pthread.h>
#endif
#endif
//
// Lock
//
// A fast mutex lock for use from mypyc-compiled code.
//
// On Python 3.14+ (all platforms), this uses CPython's PyMutex, a 1-byte atomic
// lock backed by a parking lot for contended waits. PyMutex automatically
// releases the GIL when blocking.
//
// On Python 3.13 and earlier with Windows, this uses an SRWLOCK (Slim
// Reader/Writer Lock) plus a CONDITION_VARIABLE guarding a `locked` flag. The SRWLOCK only
// protects the flag and is never held across the user's critical section, so
// release() may be called from a thread other than the acquirer (matching
// threading.Lock semantics). This mirrors CPython's Windows lock (NRMUTEX in
// Python/thread_nt.h) and is the Windows twin of the POSIX pthread+condvar
// backend below.
//
// On Python 3.13 and earlier with POSIX systems, there are two backends, both
// of which allow release() from a thread other than the one that acquired the
// lock (matching threading.Lock semantics):
//
// - Where unnamed POSIX semaphores work well (e.g. Linux) and the GIL is
// enabled, this uses a sem_t initialized to 1: acquire is sem_wait, release
// is sem_post. Semaphores have no ownership concept, so cross-thread release
// is directly well-defined.
//
// - Otherwise (e.g. macOS, whose sem_init() is a non-functional stub, and
// free-threaded builds), this uses a pthread mutex + condition variable
// guarding a `locked` flag. The mutex only protects the flag and is never
// held across the user's critical section, so the OS mutex is always
// unlocked on the same thread that locked it.
//
// ---------- Platform-specific lock state ----------
#if defined(LOCK_BACKEND_PYMUTEX)
typedef struct {
PyObject_HEAD
PyMutex mutex;
} LockObject;
#elif defined(LOCK_BACKEND_SRWLOCK)
typedef struct {
PyObject_HEAD
// The SRWLOCK below does NOT represent the Python lock; like the pthread
// fallback's `mut`, it only guards `locked` and the condition variable,
// and is held just long enough to inspect/flip the flag -- never across
// the user's critical section. That is what allows release() from a
// thread other than the acquirer: SRWLOCK's same-thread-release rule is
// never violated, and clearing `locked` is just a guarded store. This
// mirrors CPython's Windows lock (NRMUTEX in Python/thread_nt.h).
SRWLOCK srw;
CONDITION_VARIABLE lock_released;
int locked; // 0=unlocked, 1=locked; protected by `srw`
} LockObject;
#elif defined(LOCK_BACKEND_SEM)
typedef struct {
PyObject_HEAD
sem_t sem; // counting semaphore, initialized to 1
// Tracks the locked state for locked() and release-unlocked detection.
// The semaphore itself is the source of truth for mutual exclusion; this
// flag is advisory bookkeeping. It is set after a successful acquire and
// cleared before sem_post in release.
//
// This backend is only selected when the GIL is enabled, so this flag is a
// plain int relying on the GIL for serialization: it is only ever touched
// with the GIL held (the blocking sem_wait drops the GIL, but the flag
// store happens after the GIL is reacquired). This mirrors the old
// PyThread_type_lock wrapper bookkeeping used by CPython 3.12 and earlier,
// where _thread lock kept a plain `char locked` for sanity checks and
// locked().
int locked;
} LockObject;
#else // pthread mutex + condvar fallback
typedef struct {
PyObject_HEAD
// The pthread mutex below does NOT represent the Python lock; it only
// guards the `locked` flag and the condition variable. The Python lock
// state is `locked` itself. This indirection (matching CPython's POSIX
// lock) is what allows release() from a thread other than the acquirer:
// `mut` is always locked and unlocked within a single call on a single
// thread, so pthread's same-thread-unlock rule is never violated.
pthread_mutex_t mut;
pthread_cond_t lock_released;
// Always accessed while holding `mut` (including the locked() reader),
// so a plain int is sufficient -- the mutex provides the ordering.
// Matches CPython's pthread_lock.locked (a plain char).
int locked; // 0=unlocked, 1=locked; protected by `mut`
} LockObject;
#endif
// ---------- Platform-specific init/acquire/release ----------
static inline int
Lock_init_internal(LockObject *self)
{
#if defined(LOCK_BACKEND_PYMUTEX)
self->mutex = (PyMutex){0};
#elif defined(LOCK_BACKEND_SRWLOCK)
InitializeSRWLock(&self->srw);
InitializeConditionVariable(&self->lock_released);
self->locked = 0;
#elif defined(LOCK_BACKEND_SEM)
if (sem_init(&self->sem, 0, 1) != 0) {
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}
self->locked = 0;
#else
int status = pthread_mutex_init(&self->mut, NULL);
if (status != 0) {
errno = status;
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}
status = pthread_cond_init(&self->lock_released, NULL);
if (status != 0) {
pthread_mutex_destroy(&self->mut);
errno = status;
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}
self->locked = 0;
#endif
return 0;
}
// Try to acquire the lock. Returns 1 (true) on success, 0 (false) if
// non-blocking and the lock is held, or -1 if interrupted by an error-raising
// signal handler.
static int
Lock_acquire_impl(LockObject *self, int blocking)
{
#if defined(LOCK_BACKEND_PYMUTEX)
if (!blocking) {
PyLockStatus r = _PyMutex_LockTimed(&self->mutex, 0, _Py_LOCK_DONT_DETACH);
return r == PY_LOCK_ACQUIRED;
}
if (PyMutex_LockFast(&self->mutex)) {
return 1;
}
PyLockStatus r = _PyMutex_LockTimed(&self->mutex, -1,
_PY_LOCK_DETACH | _PY_LOCK_HANDLE_SIGNALS);
if (r == PY_LOCK_INTR) {
return -1;
}
return 1;
#elif defined(LOCK_BACKEND_SRWLOCK)
// `srw` only guards `locked` and the condition variable; it is held just
// long enough to inspect/flip the flag, never across the user's critical
// section. This is what lets a different thread call release().
//
// Fast path: grab the lock without releasing the GIL if it is free. This is
// also the whole story in the non-blocking case.
AcquireSRWLockExclusive(&self->srw);
if (!self->locked) {
self->locked = 1;
ReleaseSRWLockExclusive(&self->srw);
return 1;
}
ReleaseSRWLockExclusive(&self->srw);
if (!blocking) {
return 0;
}
// Slow path: wait for the lock to be released, with the GIL dropped so
// other Python threads can run (and release the lock).
// SleepConditionVariableSRW atomically releases the SRWLOCK while sleeping
// and reacquires it on wake, exactly like pthread_cond_wait.
Py_BEGIN_ALLOW_THREADS
AcquireSRWLockExclusive(&self->srw);
while (self->locked) {
SleepConditionVariableSRW(&self->lock_released, &self->srw, INFINITE, 0);
}
self->locked = 1;
ReleaseSRWLockExclusive(&self->srw);
Py_END_ALLOW_THREADS
return 1;
#elif defined(LOCK_BACKEND_SEM)
// A semaphore has no ownership: any thread may sem_post a token that
// another thread consumed via sem_wait, so cross-thread release is
// directly well-defined. `locked` is advisory bookkeeping for locked()
// and for guarding against releasing an unheld lock.
//
// Fast path: try a non-blocking acquire first to avoid GIL release/reacquire
// overhead in the common uncontended case. This is also the whole story in
// the non-blocking case.
{
int status;
do {
status = sem_trywait(&self->sem);
} while (status == -1 && errno == EINTR);
if (status == 0) {
self->locked = 1;
return 1;
}
}
if (!blocking) {
return 0; // EAGAIN: already held
}
// Slow path: block with the GIL dropped so other Python threads can run
// (and release the lock). If a signal interrupts sem_wait(), run pending
// Python signal handlers with the GIL held; retry unless a handler raises.
for (;;) {
int status;
int err = 0;
Py_BEGIN_ALLOW_THREADS
status = sem_wait(&self->sem);
if (status == -1) {
err = errno;
}
Py_END_ALLOW_THREADS
if (status == 0) {
self->locked = 1;
return 1;
}
if (err != EINTR) {
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}
if (Py_MakePendingCalls() < 0) {
return -1;
}
}
#else // pthread mutex + condvar fallback
// `mut` only guards `locked` and the condition variable; it is held just
// long enough to inspect/flip the flag, never across the user's critical
// section. This is what lets a different thread call release().
//
// Fast path: grab the lock without releasing the GIL if it is free. This is
// also the whole story in the non-blocking case.
pthread_mutex_lock(&self->mut);
if (!self->locked) {
self->locked = 1;
pthread_mutex_unlock(&self->mut);
return 1;
}
pthread_mutex_unlock(&self->mut);
if (!blocking) {
return 0;
}
// Slow path: wait for the lock to be released, with the GIL dropped so
// other Python threads can run (and release the lock). If we wake but do
// not get the lock, give pending Python signal handlers a chance to run,
// matching CPython's pthread fallback.
for (;;) {
int acquired = 0;
int interrupted = 0;
int status;
Py_BEGIN_ALLOW_THREADS
status = pthread_mutex_lock(&self->mut);
if (status == 0) {
while (self->locked) {
status = pthread_cond_wait(&self->lock_released, &self->mut);
if (status != 0) {
break;
}
if (self->locked) {
interrupted = 1;
break;
}
}
if (status == 0 && !interrupted) {
self->locked = 1;
acquired = 1;
}
int unlock_status = pthread_mutex_unlock(&self->mut);
if (status == 0) {
status = unlock_status;
}
}
Py_END_ALLOW_THREADS
if (status != 0) {
errno = status;
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}
if (acquired) {
return 1;
}
if (interrupted && Py_MakePendingCalls() < 0) {
return -1;
}
}
#endif
}
// Release the lock. Returns 0 on success, -1 if the lock was not held.
static int
Lock_release_impl(LockObject *self)
{
#if defined(LOCK_BACKEND_PYMUTEX)
// threading.Lock is unowned, so release() may be called from a different
// thread than acquire(). CPython's atomic _PyMutex_TryUnlock() is not part
// of the public API and is not exported by all CPython builds. This fast
// partial-replacement path deliberately avoids a second guard mutex:
// ordinary release() of an unlocked lock raises RuntimeError, but racy
// erroneous release() calls are undefined behavior and may make
// PyMutex_Unlock() abort.
if (!PyMutex_IsLocked(&self->mutex)) {
return -1;
}
PyMutex_Unlock(&self->mutex);
return 0;
#elif defined(LOCK_BACKEND_SRWLOCK)
AcquireSRWLockExclusive(&self->srw);
if (!self->locked) {
ReleaseSRWLockExclusive(&self->srw);
return -1;
}
self->locked = 0;
// Wake one waiter (if any). Signalling under `srw` is fine and avoids a
// lost-wakeup race.
WakeConditionVariable(&self->lock_released);
ReleaseSRWLockExclusive(&self->srw);
return 0;
#elif defined(LOCK_BACKEND_SEM)
// Check-then-clear the flag, then post. This backend is only selected when
// the GIL is enabled, so the check and clear are serialized without an
// atomic.
if (!self->locked) {
return -1;
}
self->locked = 0;
sem_post(&self->sem);
return 0;
#else // pthread mutex + condvar fallback
pthread_mutex_lock(&self->mut);
if (!self->locked) {
pthread_mutex_unlock(&self->mut);
return -1;
}
self->locked = 0;
// Wake one waiter (if any). Signalling under `mut` is fine and avoids a
// lost-wakeup race.
pthread_cond_signal(&self->lock_released);
pthread_mutex_unlock(&self->mut);
return 0;
#endif
}
static inline int
Lock_is_locked(LockObject *self)
{
#if defined(LOCK_BACKEND_PYMUTEX)
return PyMutex_IsLocked(&self->mutex);
#elif defined(LOCK_BACKEND_SRWLOCK)
// locked() is not expected to be on a perf-critical path, so take the
// SRWLOCK (shared) for a clean read of the guarded flag rather than
// relying on an atomic/volatile field.
AcquireSRWLockShared(&self->srw);
int result = self->locked;
ReleaseSRWLockShared(&self->srw);
return result;
#elif defined(LOCK_BACKEND_SEM)
// The flag is GIL-serialized (see the struct comment); locked() is a
// plain read, matching the old CPython _thread lock bookkeeping described
// above.
return self->locked != 0;
#else // pthread mutex + condvar fallback
// `locked` is only ever accessed under `mut`; take it for a clean read.
// locked() is not expected to be on a perf-critical path.
pthread_mutex_lock(&self->mut);
int result = self->locked;
pthread_mutex_unlock(&self->mut);
return result;
#endif
}
// ---------- Python type methods (shared across platforms) ----------
static PyTypeObject LockType;
static PyObject *
Lock_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
if (type != &LockType) {
PyErr_SetString(PyExc_TypeError, "Lock cannot be subclassed");
return NULL;
}
LockObject *self = (LockObject *)type->tp_alloc(type, 0);
if (self != NULL && Lock_init_internal(self) < 0) {
type->tp_free((PyObject *)self);
return NULL;
}
return (PyObject *)self;
}
static int
Lock_init(LockObject *self, PyObject *args, PyObject *kwds)
{
if (!PyArg_ParseTuple(args, "")) {
return -1;
}
if (kwds != NULL && PyDict_Size(kwds) > 0) {
PyErr_SetString(PyExc_TypeError,
"Lock() takes no keyword arguments");
return -1;
}
return 0;
}
static void
Lock_dealloc(LockObject *self)
{
#if defined(LOCK_BACKEND_SEM)
sem_destroy(&self->sem);
#elif defined(LOCK_BACKEND_PTHREAD)
// `mut` is only ever held transiently within a single call, so it is
// always unlocked here even if the Python lock is still "locked".
// Some pthread implementations require the cond to be destroyed first.
pthread_cond_destroy(&self->lock_released);
pthread_mutex_destroy(&self->mut);
#endif
Py_TYPE(self)->tp_free((PyObject *)self);
}
static PyObject *
Lock_acquire(LockObject *self, PyObject *const *args, Py_ssize_t nargs,
PyObject *kwnames)
{
int blocking = 1;
Py_ssize_t nkw = kwnames ? PyTuple_GET_SIZE(kwnames) : 0;
if (nargs + nkw > 1) {
PyErr_SetString(PyExc_TypeError, "acquire() takes at most 1 argument");
return NULL;
}
if (nargs == 1) {
blocking = PyObject_IsTrue(args[0]);
if (blocking < 0)
return NULL;
} else if (nkw == 1) {
PyObject *key = PyTuple_GET_ITEM(kwnames, 0);
if (PyUnicode_CompareWithASCIIString(key, "blocking") != 0) {
PyErr_Format(PyExc_TypeError,
"acquire() got an unexpected keyword argument '%U'",
key);
return NULL;
}
blocking = PyObject_IsTrue(args[0]);
if (blocking < 0)
return NULL;
}
int result = Lock_acquire_impl(self, blocking);
if (result < 0) {
return NULL;
}
return PyBool_FromLong(result);
}
static PyObject *
Lock_release(LockObject *self, PyObject *Py_UNUSED(ignored))
{
if (Lock_release_impl(self) < 0) {
PyErr_SetString(PyExc_RuntimeError, "cannot release an unlocked lock");
return NULL;
}
Py_RETURN_NONE;
}
static PyObject *
Lock_locked(LockObject *self, PyObject *Py_UNUSED(ignored))
{
return PyBool_FromLong(Lock_is_locked(self));
}
static PyObject *
Lock_enter(LockObject *self, PyObject *Py_UNUSED(ignored))
{
int result = Lock_acquire_impl(self, 1);
if (result < 0)
return NULL;
return PyBool_FromLong(result);
}
static PyObject *
Lock_exit(LockObject *self, PyObject *const *args, Py_ssize_t nargs)
{
return Lock_release(self, NULL);
}
static PyMethodDef Lock_methods[] = {
{"acquire", (PyCFunction)(void(*)(void))Lock_acquire, METH_FASTCALL | METH_KEYWORDS,
PyDoc_STR("Acquire the lock, blocking or non-blocking.\n"
"Returns True if the lock was acquired, False otherwise.")},
{"release", (PyCFunction)Lock_release, METH_NOARGS,
PyDoc_STR("Release the lock.")},
{"locked", (PyCFunction)Lock_locked, METH_NOARGS,
PyDoc_STR("Return True if the lock is currently held.")},
{"__enter__", (PyCFunction)Lock_enter, METH_NOARGS,
PyDoc_STR("Acquire the lock.")},
{"__exit__", (PyCFunction)Lock_exit, METH_FASTCALL,
PyDoc_STR("Release the lock.")},
{NULL}
};
static PyTypeObject LockType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "Lock",
.tp_doc = PyDoc_STR("A fast mutual exclusion lock"),
.tp_basicsize = sizeof(LockObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = Lock_new,
.tp_init = (initproc)Lock_init,
.tp_dealloc = (destructor)Lock_dealloc,
.tp_methods = Lock_methods,
};
static PyTypeObject *
Lock_type_internal(void) {
return &LockType;
}
// Create a new Lock object (for use from compiled code)
static PyObject *
Lock_new_internal(void) {
LockObject *self = (LockObject *)LockType.tp_alloc(&LockType, 0);
if (self != NULL && Lock_init_internal(self) < 0) {
LockType.tp_free((PyObject *)self);
return NULL;
}
return (PyObject *)self;
}
// Acquire the lock (blocking), for use from compiled code.
// Returns true on success, sets error and returns 2 (ERR_MAGIC) on failure.
static char
Lock_acquire_internal(PyObject *self) {
int result = Lock_acquire_impl((LockObject *)self, 1);
if (result < 0) {
return 2;
}
return (char)result;
}
// Acquire the lock with explicit blocking arg, for use from compiled code.
// Returns true if acquired, false otherwise. Sets error and returns 2
// (ERR_MAGIC) on failure.
static char
Lock_acquire_blocking_internal(PyObject *self, char blocking) {
int result = Lock_acquire_impl((LockObject *)self, blocking);
if (result < 0) {
return 2;
}
return (char)result;
}
// Release the lock, for use from compiled code.
// Returns 0 (None) on success, sets error and returns 2 (ERR_MAGIC) on failure.
static char
Lock_release_internal(PyObject *self) {
if (Lock_release_impl((LockObject *)self) < 0) {
PyErr_SetString(PyExc_RuntimeError, "cannot release an unlocked lock");
return 2;
}
return 0;
}
// Check if the lock is held, for use from compiled code.
static char
Lock_locked_internal(PyObject *self) {
return (char)Lock_is_locked((LockObject *)self);
}
static PyMethodDef librt_threading_module_methods[] = {
{NULL, NULL, 0, NULL}
};
static int
threading_abi_version(void) {
return LIBRT_THREADING_ABI_VERSION;
}
static int
threading_api_version(void) {
return LIBRT_THREADING_API_VERSION;
}
static int
librt_threading_module_exec(PyObject *m)
{
if (PyType_Ready(&LockType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Lock", (PyObject *)&LockType) < 0) {
return -1;
}
// Export mypyc internal C API via capsule
static void *threading_api[LIBRT_THREADING_API_LEN] = {
(void *)threading_abi_version,
(void *)threading_api_version,
(void *)Lock_type_internal,
(void *)Lock_new_internal,
(void *)Lock_acquire_internal,
(void *)Lock_release_internal,
(void *)Lock_locked_internal,
(void *)Lock_acquire_blocking_internal,
};
PyObject *c_api_object = PyCapsule_New((void *)threading_api, "librt.threading._C_API", NULL);
if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot librt_threading_module_slots[] = {
{Py_mod_exec, librt_threading_module_exec},
#ifdef Py_MOD_GIL_NOT_USED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
static PyModuleDef librt_threading_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "threading",
.m_doc = "Fast threading primitives optimized for mypyc",
.m_size = 0,
.m_methods = librt_threading_module_methods,
.m_slots = librt_threading_module_slots,
};
PyMODINIT_FUNC
PyInit_threading(void)
{
return PyModuleDef_Init(&librt_threading_module);
}