From 3c0be9cad42c9531e23a609b35086faebb766bc6 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 14 Mar 2026 14:40:56 +0000 Subject: [PATCH 01/45] Add librt.threading.Lock --- mypy/typeshed/stubs/librt/librt/threading.pyi | 16 + mypyc/build.py | 6 + mypyc/lib-rt/setup.py | 6 + mypyc/lib-rt/threading/librt_threading.c | 273 ++++++++++++++++++ mypyc/lib-rt/threading/librt_threading.h | 62 ++++ mypyc/test-data/run-threading.test | 32 ++ mypyc/test/test_run.py | 1 + 7 files changed, 396 insertions(+) create mode 100644 mypy/typeshed/stubs/librt/librt/threading.pyi create mode 100644 mypyc/lib-rt/threading/librt_threading.c create mode 100644 mypyc/lib-rt/threading/librt_threading.h create mode 100644 mypyc/test-data/run-threading.test diff --git a/mypy/typeshed/stubs/librt/librt/threading.pyi b/mypy/typeshed/stubs/librt/librt/threading.pyi new file mode 100644 index 0000000000000..a3571ac7f4745 --- /dev/null +++ b/mypy/typeshed/stubs/librt/librt/threading.pyi @@ -0,0 +1,16 @@ +from types import TracebackType +from typing import final + +@final +class Lock: + def acquire(self, blocking: bool = True) -> bool: ... + def release(self) -> None: ... + def locked(self) -> bool: ... + def __enter__(self) -> bool: ... + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + /, + ) -> None: ... diff --git a/mypyc/build.py b/mypyc/build.py index b46365d263e6a..8c6eabead17c9 100644 --- a/mypyc/build.py +++ b/mypyc/build.py @@ -127,6 +127,12 @@ class ModDesc(NamedTuple): ), ModDesc("librt.time", ["time/librt_time.c"], ["time/librt_time.h"], []), ModDesc("librt.random", ["random/librt_random.c"], ["random/librt_random.h"], ["random"]), + ModDesc( + "librt.threading", + ["threading/librt_threading.c"], + ["threading/librt_threading.h"], + ["threading"], + ), ] try: diff --git a/mypyc/lib-rt/setup.py b/mypyc/lib-rt/setup.py index 371b322ca18b2..15d31a443ff86 100644 --- a/mypyc/lib-rt/setup.py +++ b/mypyc/lib-rt/setup.py @@ -164,5 +164,11 @@ def run(self) -> None: include_dirs=["."], extra_compile_args=cflags, ), + Extension( + "librt.threading", + ["threading/librt_threading.c"], + include_dirs=[".", "threading"], + extra_compile_args=cflags, + ), ] ) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c new file mode 100644 index 0000000000000..63ea8ecf9450e --- /dev/null +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -0,0 +1,273 @@ +#include "pythoncapi_compat.h" + +#define PY_SSIZE_T_CLEAN +#include +#include "librt_threading.h" +#include "mypyc_util.h" + +#ifdef __linux__ +#include +#include +#include +#include +#endif + +// +// Lock +// +// A fast mutex lock for use from mypyc-compiled code. On Linux, this uses +// a futex-based implementation that avoids pthread overhead. The lock state +// is stored as a single atomic int: +// 0 = unlocked +// 1 = locked (no waiters) +// 2 = locked (with waiters) +// + +#ifdef MYPYC_EXPERIMENTAL + +#ifdef __linux__ + +typedef struct { + PyObject_HEAD + _Atomic int state; // 0=unlocked, 1=locked, 2=locked+contended +} LockObject; + +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) { + atomic_store_explicit(&self->state, 0, memory_order_relaxed); + } + 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; + } + + atomic_store_explicit(&self->state, 0, memory_order_relaxed); + return 0; +} + +static void +Lock_dealloc(LockObject *self) +{ + Py_TYPE(self)->tp_free((PyObject *)self); +} + +// Try to acquire the lock. Returns 1 (true) on success, 0 (false) if non-blocking +// and the lock is held. +static int +Lock_acquire_impl(LockObject *self, int blocking) +{ + // Fast path: try to grab the lock (0 -> 1) + int expected = 0; + if (atomic_compare_exchange_strong_explicit(&self->state, &expected, 1, + memory_order_acquire, + memory_order_relaxed)) { + return 1; + } + + if (!blocking) { + return 0; + } + + // Slow path: contended lock + for (;;) { + // If state was 1, set it to 2 (contended) so release knows to wake + if (expected == 1) { + expected = atomic_exchange_explicit(&self->state, 2, memory_order_acquire); + if (expected == 0) { + // We got the lock (it was released between the CAS and exchange) + return 1; + } + } + // Wait on the futex (only sleep if state is still 2) + Py_BEGIN_ALLOW_THREADS + syscall(SYS_futex, &self->state, FUTEX_WAIT_PRIVATE, 2, NULL, NULL, 0); + Py_END_ALLOW_THREADS + + // Try to acquire again, setting state to 2 since we know there are waiters + expected = atomic_exchange_explicit(&self->state, 2, memory_order_acquire); + if (expected == 0) { + return 1; + } + } +} + +static PyObject * +Lock_acquire(LockObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + int blocking = 1; + if (nargs > 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; + } + + int result = Lock_acquire_impl(self, blocking); + return PyBool_FromLong(result); +} + +static PyObject * +Lock_release(LockObject *self, PyObject *Py_UNUSED(ignored)) +{ + int old = atomic_exchange_explicit(&self->state, 0, memory_order_release); + if (old == 0) { + PyErr_SetString(PyExc_RuntimeError, "release unlocked lock"); + return NULL; + } + if (old == 2) { + // There are waiters, wake one + syscall(SYS_futex, &self->state, FUTEX_WAKE_PRIVATE, 1, NULL, NULL, 0); + } + Py_RETURN_NONE; +} + +static PyObject * +Lock_locked(LockObject *self, PyObject *Py_UNUSED(ignored)) +{ + int state = atomic_load_explicit(&self->state, memory_order_relaxed); + return PyBool_FromLong(state != 0); +} + +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)Lock_acquire, METH_FASTCALL, + 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; +} + +#endif // __linux__ + +#endif // MYPYC_EXPERIMENTAL + +static PyMethodDef librt_threading_module_methods[] = { + {NULL, NULL, 0, NULL} +}; + +#ifdef MYPYC_EXPERIMENTAL + +static int +threading_abi_version(void) { + return LIBRT_THREADING_ABI_VERSION; +} + +static int +threading_api_version(void) { + return LIBRT_THREADING_API_VERSION; +} + +#endif + +static int +librt_threading_module_exec(PyObject *m) +{ +#ifdef MYPYC_EXPERIMENTAL +#ifdef __linux__ + 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, + }; + 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; + } +#endif // __linux__ +#endif // MYPYC_EXPERIMENTAL + 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); +} diff --git a/mypyc/lib-rt/threading/librt_threading.h b/mypyc/lib-rt/threading/librt_threading.h new file mode 100644 index 0000000000000..29adc8d481a4d --- /dev/null +++ b/mypyc/lib-rt/threading/librt_threading.h @@ -0,0 +1,62 @@ +#ifndef LIBRT_THREADING_H +#define LIBRT_THREADING_H + +#ifndef MYPYC_EXPERIMENTAL + +static int +import_librt_threading(void) +{ + // All librt.threading features are experimental for now, so don't set up the API here + return 0; +} + +#else // MYPYC_EXPERIMENTAL + +#include + +#define LIBRT_THREADING_ABI_VERSION 1 +#define LIBRT_THREADING_API_VERSION 1 +#define LIBRT_THREADING_API_LEN 3 + +static void *LibRTThreading_API[LIBRT_THREADING_API_LEN]; + +#define LibRTThreading_ABIVersion (*(int (*)(void)) LibRTThreading_API[0]) +#define LibRTThreading_APIVersion (*(int (*)(void)) LibRTThreading_API[1]) +#define LibRTThreading_Lock_type_internal (*(PyTypeObject* (*)(void)) LibRTThreading_API[2]) + +static int +import_librt_threading(void) +{ + PyObject *mod = PyImport_ImportModule("librt.threading"); + if (mod == NULL) + return -1; + Py_DECREF(mod); // we import just for the side effect of making the below work. + void *capsule = PyCapsule_Import("librt.threading._C_API", 0); + if (capsule == NULL) + return -1; + memcpy(LibRTThreading_API, capsule, sizeof(LibRTThreading_API)); + if (LibRTThreading_ABIVersion() != LIBRT_THREADING_ABI_VERSION) { + char err[128]; + snprintf(err, sizeof(err), "ABI version conflict for librt.threading, expected %d, found %d", + LIBRT_THREADING_ABI_VERSION, + LibRTThreading_ABIVersion() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + if (LibRTThreading_APIVersion() < LIBRT_THREADING_API_VERSION) { + char err[128]; + snprintf(err, sizeof(err), + "API version conflict for librt.threading, expected %d or newer, found %d (hint: upgrade librt)", + LIBRT_THREADING_API_VERSION, + LibRTThreading_APIVersion() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + return 0; +} + +#endif // MYPYC_EXPERIMENTAL + +#endif // LIBRT_THREADING_H diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test new file mode 100644 index 0000000000000..ca2c71b5e605b --- /dev/null +++ b/mypyc/test-data/run-threading.test @@ -0,0 +1,32 @@ +# Test cases for librt.threading (compile and run) + +[case testLockBasics_librt_experimental] +from librt.threading import Lock + +def test_lock_basic() -> None: + lock = Lock() + assert not lock.locked() + assert lock.acquire() + assert lock.locked() + lock.release() + assert not lock.locked() + +def test_lock_context_manager() -> None: + lock = Lock() + with lock: + assert lock.locked() + assert not lock.locked() + +def test_lock_non_blocking() -> None: + lock = Lock() + assert lock.acquire() + assert not lock.acquire(False) + lock.release() + assert lock.acquire(False) + lock.release() + +[file driver.py] +from native import test_lock_basic, test_lock_context_manager, test_lock_non_blocking +test_lock_basic() +test_lock_context_manager() +test_lock_non_blocking() diff --git a/mypyc/test/test_run.py b/mypyc/test/test_run.py index 9004a28ebf598..868f83654b47c 100644 --- a/mypyc/test/test_run.py +++ b/mypyc/test/test_run.py @@ -82,6 +82,7 @@ "run-base64.test", "run-librt-time.test", "run-librt-random.test", + "run-threading.test", "run-match.test", "run-vecs-i64-interp.test", "run-vecs-misc-interp.test", From e94b365b414714f27f937a8471adfa524f55bf23 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 14 Mar 2026 14:48:26 +0000 Subject: [PATCH 02/45] Add test --- mypyc/test-data/run-threading.test | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index ca2c71b5e605b..66a2ba7a38d90 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -25,8 +25,20 @@ def test_lock_non_blocking() -> None: assert lock.acquire(False) lock.release() -[file driver.py] -from native import test_lock_basic, test_lock_context_manager, test_lock_non_blocking -test_lock_basic() -test_lock_context_manager() -test_lock_non_blocking() +def test_release_unlocked() -> None: + lock = Lock() + try: + lock.release() + except RuntimeError: + pass + else: + assert False, "expected RuntimeError" + # Also after acquire + release + lock.acquire() + lock.release() + try: + lock.release() + except RuntimeError: + pass + else: + assert False, "expected RuntimeError" From 36e91e1209d1d53a884d93824d411f7bb1177d0e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 14 Mar 2026 14:53:02 +0000 Subject: [PATCH 03/45] Add tests --- mypyc/test-data/run-threading.test | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index 66a2ba7a38d90..d46abd4cfa070 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -25,6 +25,42 @@ def test_lock_non_blocking() -> None: assert lock.acquire(False) lock.release() +def test_contention() -> None: + import threading + lock = Lock() + counter = [0] + n_threads = 4 + n_increments = 10000 + + def worker() -> None: + for _ in range(n_increments): + lock.acquire() + counter[0] += 1 + lock.release() + + threads = [threading.Thread(target=worker) for _ in range(n_threads)] + for t in threads: + t.start() + for t in threads: + t.join() + assert counter[0] == n_threads * n_increments + +def test_context_manager_exception() -> None: + lock = Lock() + try: + with lock: + assert lock.locked() + raise ValueError("test") + except ValueError: + pass + assert not lock.locked() + +def test_acquire_blocking_true() -> None: + lock = Lock() + assert lock.acquire(True) + assert lock.locked() + lock.release() + def test_release_unlocked() -> None: lock = Lock() try: From 5cb9aa24037511f948ea8b090d673c39ab565cde Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 14 Mar 2026 14:54:42 +0000 Subject: [PATCH 04/45] Add macOS support --- mypyc/lib-rt/threading/librt_threading.c | 180 +++++++++++++++++------ 1 file changed, 131 insertions(+), 49 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 63ea8ecf9450e..1aecc16e544a2 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -10,21 +10,33 @@ #include #include #include +#elif defined(__APPLE__) +#include +#include #endif // // Lock // -// A fast mutex lock for use from mypyc-compiled code. On Linux, this uses -// a futex-based implementation that avoids pthread overhead. The lock state -// is stored as a single atomic int: +// A fast mutex lock for use from mypyc-compiled code. +// +// On Linux, this uses a futex-based implementation. The lock state is stored +// as a single atomic int: // 0 = unlocked // 1 = locked (no waiters) // 2 = locked (with waiters) // +// On macOS, this uses os_unfair_lock (a lightweight spin-then-wait lock +// provided by the kernel). A separate atomic flag tracks the locked state +// for locked() and release-unlocked-lock detection. +// #ifdef MYPYC_EXPERIMENTAL +#if defined(__linux__) || defined(__APPLE__) + +// ---------- Platform-specific lock state ---------- + #ifdef __linux__ typedef struct { @@ -32,51 +44,35 @@ typedef struct { _Atomic int state; // 0=unlocked, 1=locked, 2=locked+contended } LockObject; -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; - } +#elif defined(__APPLE__) - LockObject *self = (LockObject *)type->tp_alloc(type, 0); - if (self != NULL) { - atomic_store_explicit(&self->state, 0, memory_order_relaxed); - } - return (PyObject *)self; -} - -static int -Lock_init(LockObject *self, PyObject *args, PyObject *kwds) -{ - if (!PyArg_ParseTuple(args, "")) { - return -1; - } +typedef struct { + PyObject_HEAD + os_unfair_lock lock; + _Atomic int locked; // 0=unlocked, 1=locked (for locked() and error checking) +} LockObject; - if (kwds != NULL && PyDict_Size(kwds) > 0) { - PyErr_SetString(PyExc_TypeError, - "Lock() takes no keyword arguments"); - return -1; - } +#endif - atomic_store_explicit(&self->state, 0, memory_order_relaxed); - return 0; -} +// ---------- Platform-specific init/acquire/release ---------- -static void -Lock_dealloc(LockObject *self) +static inline void +Lock_init_internal(LockObject *self) { - Py_TYPE(self)->tp_free((PyObject *)self); +#ifdef __linux__ + atomic_store_explicit(&self->state, 0, memory_order_relaxed); +#elif defined(__APPLE__) + self->lock = OS_UNFAIR_LOCK_INIT; + atomic_store_explicit(&self->locked, 0, memory_order_relaxed); +#endif } -// Try to acquire the lock. Returns 1 (true) on success, 0 (false) if non-blocking -// and the lock is held. +// Try to acquire the lock. Returns 1 (true) on success, 0 (false) if +// non-blocking and the lock is held. static int Lock_acquire_impl(LockObject *self, int blocking) { +#ifdef __linux__ // Fast path: try to grab the lock (0 -> 1) int expected = 0; if (atomic_compare_exchange_strong_explicit(&self->state, &expected, 1, @@ -110,6 +106,98 @@ Lock_acquire_impl(LockObject *self, int blocking) return 1; } } + +#elif defined(__APPLE__) + if (!blocking) { + if (os_unfair_lock_trylock(&self->lock)) { + atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + return 1; + } + return 0; + } + + Py_BEGIN_ALLOW_THREADS + os_unfair_lock_lock(&self->lock); + Py_END_ALLOW_THREADS + atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + return 1; +#endif +} + +// Release the lock. Returns 0 on success, -1 if the lock was not held. +static int +Lock_release_impl(LockObject *self) +{ +#ifdef __linux__ + int old = atomic_exchange_explicit(&self->state, 0, memory_order_release); + if (old == 0) { + return -1; + } + if (old == 2) { + // There are waiters, wake one + syscall(SYS_futex, &self->state, FUTEX_WAKE_PRIVATE, 1, NULL, NULL, 0); + } + return 0; + +#elif defined(__APPLE__) + if (!atomic_exchange_explicit(&self->locked, 0, memory_order_relaxed)) { + return -1; + } + os_unfair_lock_unlock(&self->lock); + return 0; +#endif +} + +static inline int +Lock_is_locked(LockObject *self) +{ +#ifdef __linux__ + return atomic_load_explicit(&self->state, memory_order_relaxed) != 0; +#elif defined(__APPLE__) + return atomic_load_explicit(&self->locked, memory_order_relaxed) != 0; +#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); + } + 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; + } + + Lock_init_internal(self); + return 0; +} + +static void +Lock_dealloc(LockObject *self) +{ + Py_TYPE(self)->tp_free((PyObject *)self); } static PyObject * @@ -133,23 +221,17 @@ Lock_acquire(LockObject *self, PyObject *const *args, Py_ssize_t nargs) static PyObject * Lock_release(LockObject *self, PyObject *Py_UNUSED(ignored)) { - int old = atomic_exchange_explicit(&self->state, 0, memory_order_release); - if (old == 0) { + if (Lock_release_impl(self) < 0) { PyErr_SetString(PyExc_RuntimeError, "release unlocked lock"); return NULL; } - if (old == 2) { - // There are waiters, wake one - syscall(SYS_futex, &self->state, FUTEX_WAKE_PRIVATE, 1, NULL, NULL, 0); - } Py_RETURN_NONE; } static PyObject * Lock_locked(LockObject *self, PyObject *Py_UNUSED(ignored)) { - int state = atomic_load_explicit(&self->state, memory_order_relaxed); - return PyBool_FromLong(state != 0); + return PyBool_FromLong(Lock_is_locked(self)); } static PyObject * @@ -200,7 +282,7 @@ Lock_type_internal(void) { return &LockType; } -#endif // __linux__ +#endif // defined(__linux__) || defined(__APPLE__) #endif // MYPYC_EXPERIMENTAL @@ -226,7 +308,7 @@ static int librt_threading_module_exec(PyObject *m) { #ifdef MYPYC_EXPERIMENTAL -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) if (PyType_Ready(&LockType) < 0) { return -1; } @@ -244,7 +326,7 @@ librt_threading_module_exec(PyObject *m) if (PyModule_Add(m, "_C_API", c_api_object) < 0) { return -1; } -#endif // __linux__ +#endif // defined(__linux__) || defined(__APPLE__) #endif // MYPYC_EXPERIMENTAL return 0; } From 420ca6b6bf0519ce37492f8e64608e52dde001cb Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 14 Mar 2026 14:56:45 +0000 Subject: [PATCH 05/45] Add generic pthread implementation --- mypyc/lib-rt/threading/librt_threading.c | 50 ++++++++++++++++++++---- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 1aecc16e544a2..6874f8e323c9c 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -13,6 +13,9 @@ #elif defined(__APPLE__) #include #include +#else +#include +#include #endif // @@ -30,11 +33,12 @@ // provided by the kernel). A separate atomic flag tracks the locked state // for locked() and release-unlocked-lock detection. // +// On other POSIX systems, this falls back to pthread_mutex with a separate +// atomic flag for locked() and release-unlocked-lock detection. +// #ifdef MYPYC_EXPERIMENTAL -#if defined(__linux__) || defined(__APPLE__) - // ---------- Platform-specific lock state ---------- #ifdef __linux__ @@ -52,6 +56,14 @@ typedef struct { _Atomic int locked; // 0=unlocked, 1=locked (for locked() and error checking) } LockObject; +#else // POSIX fallback + +typedef struct { + PyObject_HEAD + pthread_mutex_t mutex; + _Atomic int locked; // 0=unlocked, 1=locked (for locked() and error checking) +} LockObject; + #endif // ---------- Platform-specific init/acquire/release ---------- @@ -64,6 +76,9 @@ Lock_init_internal(LockObject *self) #elif defined(__APPLE__) self->lock = OS_UNFAIR_LOCK_INIT; atomic_store_explicit(&self->locked, 0, memory_order_relaxed); +#else + pthread_mutex_init(&self->mutex, NULL); + atomic_store_explicit(&self->locked, 0, memory_order_relaxed); #endif } @@ -121,6 +136,21 @@ Lock_acquire_impl(LockObject *self, int blocking) Py_END_ALLOW_THREADS atomic_store_explicit(&self->locked, 1, memory_order_relaxed); return 1; + +#else // POSIX fallback + if (!blocking) { + if (pthread_mutex_trylock(&self->mutex) == 0) { + atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + return 1; + } + return 0; + } + + Py_BEGIN_ALLOW_THREADS + pthread_mutex_lock(&self->mutex); + Py_END_ALLOW_THREADS + atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + return 1; #endif } @@ -145,6 +175,13 @@ Lock_release_impl(LockObject *self) } os_unfair_lock_unlock(&self->lock); return 0; + +#else // POSIX fallback + if (!atomic_exchange_explicit(&self->locked, 0, memory_order_relaxed)) { + return -1; + } + pthread_mutex_unlock(&self->mutex); + return 0; #endif } @@ -153,7 +190,7 @@ Lock_is_locked(LockObject *self) { #ifdef __linux__ return atomic_load_explicit(&self->state, memory_order_relaxed) != 0; -#elif defined(__APPLE__) +#else return atomic_load_explicit(&self->locked, memory_order_relaxed) != 0; #endif } @@ -197,6 +234,9 @@ Lock_init(LockObject *self, PyObject *args, PyObject *kwds) static void Lock_dealloc(LockObject *self) { +#if !defined(__linux__) && !defined(__APPLE__) + pthread_mutex_destroy(&self->mutex); +#endif Py_TYPE(self)->tp_free((PyObject *)self); } @@ -282,8 +322,6 @@ Lock_type_internal(void) { return &LockType; } -#endif // defined(__linux__) || defined(__APPLE__) - #endif // MYPYC_EXPERIMENTAL static PyMethodDef librt_threading_module_methods[] = { @@ -308,7 +346,6 @@ static int librt_threading_module_exec(PyObject *m) { #ifdef MYPYC_EXPERIMENTAL -#if defined(__linux__) || defined(__APPLE__) if (PyType_Ready(&LockType) < 0) { return -1; } @@ -326,7 +363,6 @@ librt_threading_module_exec(PyObject *m) if (PyModule_Add(m, "_C_API", c_api_object) < 0) { return -1; } -#endif // defined(__linux__) || defined(__APPLE__) #endif // MYPYC_EXPERIMENTAL return 0; } From e17290be1dd0c53e0388cdeba05793bd57c0cf7a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 14 Mar 2026 15:03:16 +0000 Subject: [PATCH 06/45] Support Windows (untested) --- mypyc/lib-rt/threading/librt_threading.c | 73 ++++++++++++++++++++---- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 6874f8e323c9c..2b051e92c5d12 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -5,15 +5,26 @@ #include "librt_threading.h" #include "mypyc_util.h" -#ifdef __linux__ +// Select lock backend: +// LOCK_BACKEND_FUTEX - Linux futex (fastest on Linux) +// LOCK_BACKEND_UNFAIR - macOS os_unfair_lock +// LOCK_BACKEND_SRWLOCK - Windows Slim Reader/Writer Lock +// LOCK_BACKEND_PTHREAD - POSIX fallback +#if defined(__linux__) && !defined(LOCK_FORCE_PTHREAD) +#define LOCK_BACKEND_FUTEX #include #include #include #include -#elif defined(__APPLE__) +#elif defined(__APPLE__) && !defined(LOCK_FORCE_PTHREAD) +#define LOCK_BACKEND_UNFAIR #include #include +#elif defined(_WIN32) +#define LOCK_BACKEND_SRWLOCK +#include #else +#define LOCK_BACKEND_PTHREAD #include #include #endif @@ -33,6 +44,9 @@ // provided by the kernel). A separate atomic flag tracks the locked state // for locked() and release-unlocked-lock detection. // +// On Windows, this uses SRWLOCK (Slim Reader/Writer Lock), a lightweight +// kernel primitive. A separate volatile flag tracks the locked state. +// // On other POSIX systems, this falls back to pthread_mutex with a separate // atomic flag for locked() and release-unlocked-lock detection. // @@ -41,14 +55,14 @@ // ---------- Platform-specific lock state ---------- -#ifdef __linux__ +#ifdef LOCK_BACKEND_FUTEX typedef struct { PyObject_HEAD _Atomic int state; // 0=unlocked, 1=locked, 2=locked+contended } LockObject; -#elif defined(__APPLE__) +#elif defined(LOCK_BACKEND_UNFAIR) typedef struct { PyObject_HEAD @@ -56,6 +70,14 @@ typedef struct { _Atomic int locked; // 0=unlocked, 1=locked (for locked() and error checking) } LockObject; +#elif defined(LOCK_BACKEND_SRWLOCK) + +typedef struct { + PyObject_HEAD + SRWLOCK lock; + volatile long locked; // 0=unlocked, 1=locked (for locked() and error checking) +} LockObject; + #else // POSIX fallback typedef struct { @@ -71,11 +93,14 @@ typedef struct { static inline void Lock_init_internal(LockObject *self) { -#ifdef __linux__ +#ifdef LOCK_BACKEND_FUTEX atomic_store_explicit(&self->state, 0, memory_order_relaxed); -#elif defined(__APPLE__) +#elif defined(LOCK_BACKEND_UNFAIR) self->lock = OS_UNFAIR_LOCK_INIT; atomic_store_explicit(&self->locked, 0, memory_order_relaxed); +#elif defined(LOCK_BACKEND_SRWLOCK) + InitializeSRWLock(&self->lock); + self->locked = 0; #else pthread_mutex_init(&self->mutex, NULL); atomic_store_explicit(&self->locked, 0, memory_order_relaxed); @@ -87,7 +112,7 @@ Lock_init_internal(LockObject *self) static int Lock_acquire_impl(LockObject *self, int blocking) { -#ifdef __linux__ +#ifdef LOCK_BACKEND_FUTEX // Fast path: try to grab the lock (0 -> 1) int expected = 0; if (atomic_compare_exchange_strong_explicit(&self->state, &expected, 1, @@ -122,7 +147,7 @@ Lock_acquire_impl(LockObject *self, int blocking) } } -#elif defined(__APPLE__) +#elif defined(LOCK_BACKEND_UNFAIR) if (!blocking) { if (os_unfair_lock_trylock(&self->lock)) { atomic_store_explicit(&self->locked, 1, memory_order_relaxed); @@ -137,6 +162,21 @@ Lock_acquire_impl(LockObject *self, int blocking) atomic_store_explicit(&self->locked, 1, memory_order_relaxed); return 1; +#elif defined(LOCK_BACKEND_SRWLOCK) + if (!blocking) { + if (TryAcquireSRWLockExclusive(&self->lock)) { + InterlockedExchange(&self->locked, 1); + return 1; + } + return 0; + } + + Py_BEGIN_ALLOW_THREADS + AcquireSRWLockExclusive(&self->lock); + Py_END_ALLOW_THREADS + InterlockedExchange(&self->locked, 1); + return 1; + #else // POSIX fallback if (!blocking) { if (pthread_mutex_trylock(&self->mutex) == 0) { @@ -158,7 +198,7 @@ Lock_acquire_impl(LockObject *self, int blocking) static int Lock_release_impl(LockObject *self) { -#ifdef __linux__ +#ifdef LOCK_BACKEND_FUTEX int old = atomic_exchange_explicit(&self->state, 0, memory_order_release); if (old == 0) { return -1; @@ -169,13 +209,20 @@ Lock_release_impl(LockObject *self) } return 0; -#elif defined(__APPLE__) +#elif defined(LOCK_BACKEND_UNFAIR) if (!atomic_exchange_explicit(&self->locked, 0, memory_order_relaxed)) { return -1; } os_unfair_lock_unlock(&self->lock); return 0; +#elif defined(LOCK_BACKEND_SRWLOCK) + if (!InterlockedExchange(&self->locked, 0)) { + return -1; + } + ReleaseSRWLockExclusive(&self->lock); + return 0; + #else // POSIX fallback if (!atomic_exchange_explicit(&self->locked, 0, memory_order_relaxed)) { return -1; @@ -188,8 +235,10 @@ Lock_release_impl(LockObject *self) static inline int Lock_is_locked(LockObject *self) { -#ifdef __linux__ +#ifdef LOCK_BACKEND_FUTEX return atomic_load_explicit(&self->state, memory_order_relaxed) != 0; +#elif defined(LOCK_BACKEND_SRWLOCK) + return InterlockedCompareExchange(&self->locked, 0, 0) != 0; #else return atomic_load_explicit(&self->locked, memory_order_relaxed) != 0; #endif @@ -234,7 +283,7 @@ Lock_init(LockObject *self, PyObject *args, PyObject *kwds) static void Lock_dealloc(LockObject *self) { -#if !defined(__linux__) && !defined(__APPLE__) +#ifdef LOCK_BACKEND_PTHREAD pthread_mutex_destroy(&self->mutex); #endif Py_TYPE(self)->tp_free((PyObject *)self); From 4c864b95ab8dd9f214ff6ad8f3543a2260ed52ab Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 14 Mar 2026 15:59:29 +0000 Subject: [PATCH 07/45] Add some primitives --- mypyc/codegen/emitmodule.py | 5 ++ mypyc/ir/deps.py | 1 + mypyc/ir/rtypes.py | 8 ++- mypyc/lib-rt/threading/librt_threading.c | 39 ++++++++++++ mypyc/lib-rt/threading/librt_threading.h | 8 ++- mypyc/primitives/librt_threading_ops.py | 48 +++++++++++++++ mypyc/primitives/registry.py | 1 + mypyc/test-data/irbuild-threading.test | 76 ++++++++++++++++++++++++ mypyc/test/test_irbuild.py | 1 + 9 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 mypyc/primitives/librt_threading_ops.py create mode 100644 mypyc/test-data/irbuild-threading.test diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index 6e3c122aa13f6..3b320b5321232 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -62,6 +62,7 @@ LIBRT_BASE64, LIBRT_RANDOM, LIBRT_STRINGS, + LIBRT_THREADING, LIBRT_TIME, LIBRT_VECS, Capsule, @@ -1261,6 +1262,10 @@ def emit_module_exec_func( emitter.emit_line("if (import_librt_time() < 0) {") emitter.emit_line("return -1;") emitter.emit_line("}") + if LIBRT_THREADING in module.dependencies: + emitter.emit_line("if (import_librt_threading() < 0) {") + emitter.emit_line("return -1;") + emitter.emit_line("}") if LIBRT_VECS in module.dependencies: emitter.emit_line("if (import_librt_vecs() < 0) {") emitter.emit_line("return -1;") diff --git a/mypyc/ir/deps.py b/mypyc/ir/deps.py index 751845d3a324c..b1a27f85fcfe6 100644 --- a/mypyc/ir/deps.py +++ b/mypyc/ir/deps.py @@ -110,6 +110,7 @@ def get_header(self) -> str: LIBRT_VECS: Final = Capsule("librt.vecs") LIBRT_TIME: Final = Capsule("librt.time") LIBRT_RANDOM: Final = Capsule("librt.random") +LIBRT_THREADING: Final = Capsule("librt.threading") BYTES_EXTRA_OPS: Final = SourceDep("bytes_extra_ops.c") BYTES_WRITER_EXTRA_OPS: Final = SourceDep("byteswriter_extra_ops.c") diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 9d13ecc83175d..dd1ae5cac5f31 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -41,7 +41,7 @@ class to enable the new behavior. In rare cases, adding a new from typing import TYPE_CHECKING, ClassVar, Final, Generic, TypeGuard, TypeVar, Union, final from mypyc.common import HAVE_IMMORTAL, IS_32_BIT_PLATFORM, PLATFORM_SIZE, JsonDict, short_name -from mypyc.ir.deps import LIBRT_RANDOM, LIBRT_STRINGS, LIBRT_VECS, Dependency +from mypyc.ir.deps import LIBRT_RANDOM, LIBRT_STRINGS, LIBRT_THREADING, LIBRT_VECS, Dependency from mypyc.namegen import NameGenerator if TYPE_CHECKING: @@ -550,12 +550,16 @@ def __hash__(self) -> int: } | { "librt.random.Random": RPrimitive( "librt.random.Random", is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_RANDOM,) - ) + ), + "librt.threading.Lock": RPrimitive( + "librt.threading.Lock", is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_THREADING,) + ), } bytes_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.BytesWriter"] string_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.StringWriter"] random_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.random.Random"] +lock_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.threading.Lock"] def is_native_rprimitive(rtype: RType) -> bool: diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 2b051e92c5d12..4d3d5646daed7 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -371,6 +371,41 @@ 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); + } + return (PyObject *)self; +} + +// Acquire the lock (blocking), for use from compiled code. +// Returns true on success. +static char +Lock_acquire_internal(PyObject *self) { + int result = Lock_acquire_impl((LockObject *)self, 1); + 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, "release 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); +} + #endif // MYPYC_EXPERIMENTAL static PyMethodDef librt_threading_module_methods[] = { @@ -407,6 +442,10 @@ librt_threading_module_exec(PyObject *m) (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, }; PyObject *c_api_object = PyCapsule_New((void *)threading_api, "librt.threading._C_API", NULL); if (PyModule_Add(m, "_C_API", c_api_object) < 0) { diff --git a/mypyc/lib-rt/threading/librt_threading.h b/mypyc/lib-rt/threading/librt_threading.h index 29adc8d481a4d..784a82108c4fe 100644 --- a/mypyc/lib-rt/threading/librt_threading.h +++ b/mypyc/lib-rt/threading/librt_threading.h @@ -15,14 +15,18 @@ import_librt_threading(void) #include #define LIBRT_THREADING_ABI_VERSION 1 -#define LIBRT_THREADING_API_VERSION 1 -#define LIBRT_THREADING_API_LEN 3 +#define LIBRT_THREADING_API_VERSION 2 +#define LIBRT_THREADING_API_LEN 7 static void *LibRTThreading_API[LIBRT_THREADING_API_LEN]; #define LibRTThreading_ABIVersion (*(int (*)(void)) LibRTThreading_API[0]) #define LibRTThreading_APIVersion (*(int (*)(void)) LibRTThreading_API[1]) #define LibRTThreading_Lock_type_internal (*(PyTypeObject* (*)(void)) LibRTThreading_API[2]) +#define LibRTThreading_Lock_new_internal (*(PyObject* (*)(void)) LibRTThreading_API[3]) +#define LibRTThreading_Lock_acquire_internal (*(char (*)(PyObject *self)) LibRTThreading_API[4]) +#define LibRTThreading_Lock_release_internal (*(char (*)(PyObject *self)) LibRTThreading_API[5]) +#define LibRTThreading_Lock_locked_internal (*(char (*)(PyObject *self)) LibRTThreading_API[6]) static int import_librt_threading(void) diff --git a/mypyc/primitives/librt_threading_ops.py b/mypyc/primitives/librt_threading_ops.py new file mode 100644 index 0000000000000..01b520d5cb661 --- /dev/null +++ b/mypyc/primitives/librt_threading_ops.py @@ -0,0 +1,48 @@ +from mypyc.ir.deps import LIBRT_THREADING +from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER +from mypyc.ir.rtypes import bool_rprimitive, lock_rprimitive, none_rprimitive +from mypyc.primitives.registry import function_op, method_op + +# Lock() +function_op( + name="librt.threading.Lock", + arg_types=[], + return_type=lock_rprimitive, + c_function_name="LibRTThreading_Lock_new_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_THREADING], +) + +# Lock.acquire() -- blocking acquire, always returns True +method_op( + name="acquire", + arg_types=[lock_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTThreading_Lock_acquire_internal", + error_kind=ERR_NEVER, + experimental=True, + dependencies=[LIBRT_THREADING], +) + +# Lock.release() +method_op( + name="release", + arg_types=[lock_rprimitive], + return_type=none_rprimitive, + c_function_name="LibRTThreading_Lock_release_internal", + error_kind=ERR_MAGIC, + experimental=True, + dependencies=[LIBRT_THREADING], +) + +# Lock.locked() +method_op( + name="locked", + arg_types=[lock_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTThreading_Lock_locked_internal", + error_kind=ERR_NEVER, + experimental=True, + dependencies=[LIBRT_THREADING], +) diff --git a/mypyc/primitives/registry.py b/mypyc/primitives/registry.py index 22422987b4277..060af0f88020c 100644 --- a/mypyc/primitives/registry.py +++ b/mypyc/primitives/registry.py @@ -411,6 +411,7 @@ def load_global_op(name: str, type: RType, src: str) -> LoadAddressDescription: import mypyc.primitives.int_ops import mypyc.primitives.librt_random_ops import mypyc.primitives.librt_strings_ops +import mypyc.primitives.librt_threading_ops import mypyc.primitives.librt_time_ops import mypyc.primitives.librt_vecs_ops import mypyc.primitives.list_ops diff --git a/mypyc/test-data/irbuild-threading.test b/mypyc/test-data/irbuild-threading.test new file mode 100644 index 0000000000000..b6cc2601164df --- /dev/null +++ b/mypyc/test-data/irbuild-threading.test @@ -0,0 +1,76 @@ +[case testLockBasics_experimental] +from librt.threading import Lock + +def lock_create() -> Lock: + return Lock() + +def lock_acquire(lk: Lock) -> bool: + return lk.acquire() + +def lock_release(lk: Lock) -> None: + lk.release() + +def lock_locked(lk: Lock) -> bool: + return lk.locked() +[out] +def lock_create(): + r0 :: librt.threading.Lock +L0: + r0 = LibRTThreading_Lock_new_internal() + return r0 +def lock_acquire(lk): + lk :: librt.threading.Lock + r0 :: bool +L0: + r0 = LibRTThreading_Lock_acquire_internal(lk) + return r0 +def lock_release(lk): + lk :: librt.threading.Lock + r0 :: None +L0: + r0 = LibRTThreading_Lock_release_internal(lk) + return 1 +def lock_locked(lk): + lk :: librt.threading.Lock + r0 :: bool +L0: + r0 = LibRTThreading_Lock_locked_internal(lk) + return r0 + +[case testLockAcquireRelease_experimental] +from librt.threading import Lock + +def acquire_release() -> None: + lk = Lock() + lk.acquire() + lk.release() +[out] +def acquire_release(): + r0, lk :: librt.threading.Lock + r1 :: bool + r2 :: None +L0: + r0 = LibRTThreading_Lock_new_internal() + lk = r0 + r1 = LibRTThreading_Lock_acquire_internal(lk) + r2 = LibRTThreading_Lock_release_internal(lk) + return 1 + +[case testLockExperimentalDisabled] +from librt.threading import Lock + +def create_lock() -> Lock: + return Lock() +[out] +def create_lock(): + r0 :: dict + r1 :: str + r2, r3 :: object + r4 :: librt.threading.Lock +L0: + r0 = __main__.globals :: static + r1 = 'Lock' + r2 = CPyDict_GetItem(r0, r1) + r3 = PyObject_Vectorcall(r2, 0, 0, 0) + r4 = cast(librt.threading.Lock, r3) + return r4 diff --git a/mypyc/test/test_irbuild.py b/mypyc/test/test_irbuild.py index 7e3993e267e74..c0d3027e2a81c 100644 --- a/mypyc/test/test_irbuild.py +++ b/mypyc/test/test_irbuild.py @@ -61,6 +61,7 @@ "irbuild-librt-strings.test", "irbuild-librt-random.test", "irbuild-base64.test", + "irbuild-threading.test", "irbuild-time.test", "irbuild-match.test", ] From e33bd42ccae5feffc7291915e292448fe530f605 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 14 Mar 2026 16:10:19 +0000 Subject: [PATCH 08/45] Optimize 'with' statement --- mypyc/irbuild/statement.py | 37 ++++++++++++++++++++ mypyc/primitives/librt_threading_ops.py | 6 ++-- mypyc/test-data/irbuild-threading.test | 45 +++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index 428650a810dca..60d803d3aed30 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -80,6 +80,7 @@ c_pyssize_t_rprimitive, exc_rtuple, is_tagged, + lock_rprimitive, none_rprimitive, object_pointer_rprimitive, object_rprimitive, @@ -1108,6 +1109,10 @@ def transform_with( al = "a" if is_async else "" mgr_v = builder.accept(expr) + + if not is_async and mgr_v.type == lock_rprimitive: + transform_with_lock(builder, mgr_v, target, body, line) + return is_native = isinstance(mgr_v.type, RInstance) if is_native: value = builder.add(MethodCall(mgr_v, f"__{al}enter__", args=[], line=line)) @@ -1179,6 +1184,38 @@ def finally_body() -> None: ) +def transform_with_lock( + builder: IRBuilder, + mgr_v: Value, + target: Lvalue | None, + body: GenFunc, + line: int, +) -> None: + """Optimized 'with' for librt.threading.Lock. + + Generate a simple try/finally with direct acquire/release calls. + Lock.__exit__ never suppresses exceptions, so we don't need the + full PEP 343 try/except/finally machinery. + """ + from mypyc.primitives.librt_threading_ops import lock_acquire_op, lock_release_op + + # __enter__: acquire the lock + value = builder.primitive_op(lock_acquire_op, [mgr_v], line) + + mgr = builder.maybe_spill(mgr_v) + + def try_body() -> None: + if target: + builder.assign(builder.get_assignment_target(target), value, line) + body() + + def finally_body() -> None: + # __exit__: release the lock (ignoring exception info) + builder.primitive_op(lock_release_op, [builder.read(mgr, line)], line) + + transform_try_finally_stmt(builder, try_body, finally_body, line) + + def transform_with_stmt(builder: IRBuilder, o: WithStmt) -> None: # Generate separate logic for each expr in it, left to right def generate(i: int) -> None: diff --git a/mypyc/primitives/librt_threading_ops.py b/mypyc/primitives/librt_threading_ops.py index 01b520d5cb661..14affd8b5884e 100644 --- a/mypyc/primitives/librt_threading_ops.py +++ b/mypyc/primitives/librt_threading_ops.py @@ -1,7 +1,7 @@ from mypyc.ir.deps import LIBRT_THREADING from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER from mypyc.ir.rtypes import bool_rprimitive, lock_rprimitive, none_rprimitive -from mypyc.primitives.registry import function_op, method_op +from mypyc.primitives.registry import custom_primitive_op, function_op, method_op # Lock() function_op( @@ -15,7 +15,7 @@ ) # Lock.acquire() -- blocking acquire, always returns True -method_op( +lock_acquire_op = method_op( name="acquire", arg_types=[lock_rprimitive], return_type=bool_rprimitive, @@ -26,7 +26,7 @@ ) # Lock.release() -method_op( +lock_release_op = method_op( name="release", arg_types=[lock_rprimitive], return_type=none_rprimitive, diff --git a/mypyc/test-data/irbuild-threading.test b/mypyc/test-data/irbuild-threading.test index b6cc2601164df..bbcb61f81fbc0 100644 --- a/mypyc/test-data/irbuild-threading.test +++ b/mypyc/test-data/irbuild-threading.test @@ -56,6 +56,51 @@ L0: r2 = LibRTThreading_Lock_release_internal(lk) return 1 +[case testLockWith_experimental] +from librt.threading import Lock + +def with_lock(lk: Lock) -> None: + with lk: + a: list[int] = [] +[out] +def with_lock(lk): + lk :: librt.threading.Lock + r0 :: bool + r1, a :: list + r2, r3, r4 :: tuple[object, object, object] + r5 :: None + r6 :: bit +L0: + r0 = LibRTThreading_Lock_acquire_internal(lk) +L1: + r1 = PyList_New(0) + a = r1 +L2: +L3: + r2 = :: tuple[object, object, object] + r3 = r2 + goto L5 +L4: (handler for L1) + r4 = CPy_CatchError() + r3 = r4 +L5: + r5 = LibRTThreading_Lock_release_internal(lk) + if is_error(r3) goto L7 else goto L6 +L6: + CPy_Reraise() + unreachable +L7: + goto L11 +L8: (handler for L5, L6) + if is_error(r3) goto L10 else goto L9 +L9: + CPy_RestoreExcInfo(r3) +L10: + r6 = CPy_KeepPropagating() + unreachable +L11: + return 1 + [case testLockExperimentalDisabled] from librt.threading import Lock From cc874e087e87c3b7c486f033e86551841f43b3bb Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 19 Mar 2026 13:55:13 +0000 Subject: [PATCH 09/45] Use pthread on Linux --- mypyc/lib-rt/threading/librt_threading.c | 88 +++--------------------- 1 file changed, 9 insertions(+), 79 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 4d3d5646daed7..e4dd8de71b6a2 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -6,17 +6,10 @@ #include "mypyc_util.h" // Select lock backend: -// LOCK_BACKEND_FUTEX - Linux futex (fastest on Linux) // LOCK_BACKEND_UNFAIR - macOS os_unfair_lock // LOCK_BACKEND_SRWLOCK - Windows Slim Reader/Writer Lock -// LOCK_BACKEND_PTHREAD - POSIX fallback -#if defined(__linux__) && !defined(LOCK_FORCE_PTHREAD) -#define LOCK_BACKEND_FUTEX -#include -#include -#include -#include -#elif defined(__APPLE__) && !defined(LOCK_FORCE_PTHREAD) +// LOCK_BACKEND_PTHREAD - POSIX (Linux and other Unix-like systems) +#if defined(__APPLE__) #define LOCK_BACKEND_UNFAIR #include #include @@ -34,12 +27,6 @@ // // A fast mutex lock for use from mypyc-compiled code. // -// On Linux, this uses a futex-based implementation. The lock state is stored -// as a single atomic int: -// 0 = unlocked -// 1 = locked (no waiters) -// 2 = locked (with waiters) -// // On macOS, this uses os_unfair_lock (a lightweight spin-then-wait lock // provided by the kernel). A separate atomic flag tracks the locked state // for locked() and release-unlocked-lock detection. @@ -47,22 +34,15 @@ // On Windows, this uses SRWLOCK (Slim Reader/Writer Lock), a lightweight // kernel primitive. A separate volatile flag tracks the locked state. // -// On other POSIX systems, this falls back to pthread_mutex with a separate -// atomic flag for locked() and release-unlocked-lock detection. +// On other systems (Linux and other POSIX), this uses pthread_mutex with +// a separate atomic flag for locked() and release-unlocked-lock detection. // #ifdef MYPYC_EXPERIMENTAL // ---------- Platform-specific lock state ---------- -#ifdef LOCK_BACKEND_FUTEX - -typedef struct { - PyObject_HEAD - _Atomic int state; // 0=unlocked, 1=locked, 2=locked+contended -} LockObject; - -#elif defined(LOCK_BACKEND_UNFAIR) +#ifdef LOCK_BACKEND_UNFAIR typedef struct { PyObject_HEAD @@ -93,9 +73,7 @@ typedef struct { static inline void Lock_init_internal(LockObject *self) { -#ifdef LOCK_BACKEND_FUTEX - atomic_store_explicit(&self->state, 0, memory_order_relaxed); -#elif defined(LOCK_BACKEND_UNFAIR) +#ifdef LOCK_BACKEND_UNFAIR self->lock = OS_UNFAIR_LOCK_INIT; atomic_store_explicit(&self->locked, 0, memory_order_relaxed); #elif defined(LOCK_BACKEND_SRWLOCK) @@ -112,42 +90,7 @@ Lock_init_internal(LockObject *self) static int Lock_acquire_impl(LockObject *self, int blocking) { -#ifdef LOCK_BACKEND_FUTEX - // Fast path: try to grab the lock (0 -> 1) - int expected = 0; - if (atomic_compare_exchange_strong_explicit(&self->state, &expected, 1, - memory_order_acquire, - memory_order_relaxed)) { - return 1; - } - - if (!blocking) { - return 0; - } - - // Slow path: contended lock - for (;;) { - // If state was 1, set it to 2 (contended) so release knows to wake - if (expected == 1) { - expected = atomic_exchange_explicit(&self->state, 2, memory_order_acquire); - if (expected == 0) { - // We got the lock (it was released between the CAS and exchange) - return 1; - } - } - // Wait on the futex (only sleep if state is still 2) - Py_BEGIN_ALLOW_THREADS - syscall(SYS_futex, &self->state, FUTEX_WAIT_PRIVATE, 2, NULL, NULL, 0); - Py_END_ALLOW_THREADS - - // Try to acquire again, setting state to 2 since we know there are waiters - expected = atomic_exchange_explicit(&self->state, 2, memory_order_acquire); - if (expected == 0) { - return 1; - } - } - -#elif defined(LOCK_BACKEND_UNFAIR) +#ifdef LOCK_BACKEND_UNFAIR if (!blocking) { if (os_unfair_lock_trylock(&self->lock)) { atomic_store_explicit(&self->locked, 1, memory_order_relaxed); @@ -198,18 +141,7 @@ Lock_acquire_impl(LockObject *self, int blocking) static int Lock_release_impl(LockObject *self) { -#ifdef LOCK_BACKEND_FUTEX - int old = atomic_exchange_explicit(&self->state, 0, memory_order_release); - if (old == 0) { - return -1; - } - if (old == 2) { - // There are waiters, wake one - syscall(SYS_futex, &self->state, FUTEX_WAKE_PRIVATE, 1, NULL, NULL, 0); - } - return 0; - -#elif defined(LOCK_BACKEND_UNFAIR) +#ifdef LOCK_BACKEND_UNFAIR if (!atomic_exchange_explicit(&self->locked, 0, memory_order_relaxed)) { return -1; } @@ -235,9 +167,7 @@ Lock_release_impl(LockObject *self) static inline int Lock_is_locked(LockObject *self) { -#ifdef LOCK_BACKEND_FUTEX - return atomic_load_explicit(&self->state, memory_order_relaxed) != 0; -#elif defined(LOCK_BACKEND_SRWLOCK) +#ifdef LOCK_BACKEND_SRWLOCK return InterlockedCompareExchange(&self->locked, 0, 0) != 0; #else return atomic_load_explicit(&self->locked, memory_order_relaxed) != 0; From b9d2e952c6b852044caf7425404b6f3fa7611d77 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 19 Mar 2026 14:17:26 +0000 Subject: [PATCH 10/45] Use pthreads on macOS --- mypyc/lib-rt/threading/librt_threading.c | 56 +++--------------------- 1 file changed, 7 insertions(+), 49 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index e4dd8de71b6a2..bb844fdd75909 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -6,14 +6,9 @@ #include "mypyc_util.h" // Select lock backend: -// LOCK_BACKEND_UNFAIR - macOS os_unfair_lock // LOCK_BACKEND_SRWLOCK - Windows Slim Reader/Writer Lock -// LOCK_BACKEND_PTHREAD - POSIX (Linux and other Unix-like systems) -#if defined(__APPLE__) -#define LOCK_BACKEND_UNFAIR -#include -#include -#elif defined(_WIN32) +// LOCK_BACKEND_PTHREAD - POSIX (macOS, Linux, and other Unix-like systems) +#if defined(_WIN32) #define LOCK_BACKEND_SRWLOCK #include #else @@ -27,14 +22,10 @@ // // A fast mutex lock for use from mypyc-compiled code. // -// On macOS, this uses os_unfair_lock (a lightweight spin-then-wait lock -// provided by the kernel). A separate atomic flag tracks the locked state -// for locked() and release-unlocked-lock detection. -// // On Windows, this uses SRWLOCK (Slim Reader/Writer Lock), a lightweight // kernel primitive. A separate volatile flag tracks the locked state. // -// On other systems (Linux and other POSIX), this uses pthread_mutex with +// On POSIX systems (macOS, Linux, etc.), this uses pthread_mutex with // a separate atomic flag for locked() and release-unlocked-lock detection. // @@ -42,15 +33,7 @@ // ---------- Platform-specific lock state ---------- -#ifdef LOCK_BACKEND_UNFAIR - -typedef struct { - PyObject_HEAD - os_unfair_lock lock; - _Atomic int locked; // 0=unlocked, 1=locked (for locked() and error checking) -} LockObject; - -#elif defined(LOCK_BACKEND_SRWLOCK) +#ifdef LOCK_BACKEND_SRWLOCK typedef struct { PyObject_HEAD @@ -73,10 +56,7 @@ typedef struct { static inline void Lock_init_internal(LockObject *self) { -#ifdef LOCK_BACKEND_UNFAIR - self->lock = OS_UNFAIR_LOCK_INIT; - atomic_store_explicit(&self->locked, 0, memory_order_relaxed); -#elif defined(LOCK_BACKEND_SRWLOCK) +#ifdef LOCK_BACKEND_SRWLOCK InitializeSRWLock(&self->lock); self->locked = 0; #else @@ -90,22 +70,7 @@ Lock_init_internal(LockObject *self) static int Lock_acquire_impl(LockObject *self, int blocking) { -#ifdef LOCK_BACKEND_UNFAIR - if (!blocking) { - if (os_unfair_lock_trylock(&self->lock)) { - atomic_store_explicit(&self->locked, 1, memory_order_relaxed); - return 1; - } - return 0; - } - - Py_BEGIN_ALLOW_THREADS - os_unfair_lock_lock(&self->lock); - Py_END_ALLOW_THREADS - atomic_store_explicit(&self->locked, 1, memory_order_relaxed); - return 1; - -#elif defined(LOCK_BACKEND_SRWLOCK) +#ifdef LOCK_BACKEND_SRWLOCK if (!blocking) { if (TryAcquireSRWLockExclusive(&self->lock)) { InterlockedExchange(&self->locked, 1); @@ -141,14 +106,7 @@ Lock_acquire_impl(LockObject *self, int blocking) static int Lock_release_impl(LockObject *self) { -#ifdef LOCK_BACKEND_UNFAIR - if (!atomic_exchange_explicit(&self->locked, 0, memory_order_relaxed)) { - return -1; - } - os_unfair_lock_unlock(&self->lock); - return 0; - -#elif defined(LOCK_BACKEND_SRWLOCK) +#ifdef LOCK_BACKEND_SRWLOCK if (!InterlockedExchange(&self->locked, 0)) { return -1; } From 53824689be7600119e7231ef06da07fcbbf5b4ff Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 19 Mar 2026 15:37:09 +0000 Subject: [PATCH 11/45] Lint --- mypyc/ir/rtypes.py | 7 +++++-- mypyc/irbuild/statement.py | 6 +----- mypyc/primitives/librt_threading_ops.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index dd1ae5cac5f31..c28a09e85547f 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -552,8 +552,11 @@ def __hash__(self) -> int: "librt.random.Random", is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_RANDOM,) ), "librt.threading.Lock": RPrimitive( - "librt.threading.Lock", is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_THREADING,) - ), + "librt.threading.Lock", + is_unboxed=False, + is_refcounted=True, + dependencies=(LIBRT_THREADING,), + ) } bytes_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.BytesWriter"] diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index 60d803d3aed30..c73c278e2493f 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -1185,11 +1185,7 @@ def finally_body() -> None: def transform_with_lock( - builder: IRBuilder, - mgr_v: Value, - target: Lvalue | None, - body: GenFunc, - line: int, + builder: IRBuilder, mgr_v: Value, target: Lvalue | None, body: GenFunc, line: int ) -> None: """Optimized 'with' for librt.threading.Lock. diff --git a/mypyc/primitives/librt_threading_ops.py b/mypyc/primitives/librt_threading_ops.py index 14affd8b5884e..b7d863772490a 100644 --- a/mypyc/primitives/librt_threading_ops.py +++ b/mypyc/primitives/librt_threading_ops.py @@ -1,7 +1,7 @@ from mypyc.ir.deps import LIBRT_THREADING from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER from mypyc.ir.rtypes import bool_rprimitive, lock_rprimitive, none_rprimitive -from mypyc.primitives.registry import custom_primitive_op, function_op, method_op +from mypyc.primitives.registry import function_op, method_op # Lock() function_op( From 9a230a1b2d87d9324bf2674cb4077941d67d43a0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 19 Mar 2026 16:22:20 +0000 Subject: [PATCH 12/45] Optimize --- mypyc/lib-rt/threading/librt_threading.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index bb844fdd75909..39853fe836a61 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -79,6 +79,13 @@ Lock_acquire_impl(LockObject *self, int blocking) return 0; } + // Fast path: try non-blocking acquire first to avoid GIL release/reacquire + // overhead in the common uncontended case. + if (TryAcquireSRWLockExclusive(&self->lock)) { + InterlockedExchange(&self->locked, 1); + return 1; + } + Py_BEGIN_ALLOW_THREADS AcquireSRWLockExclusive(&self->lock); Py_END_ALLOW_THREADS @@ -94,6 +101,13 @@ Lock_acquire_impl(LockObject *self, int blocking) return 0; } + // Fast path: try non-blocking acquire first to avoid GIL release/reacquire + // overhead in the common uncontended case. + if (pthread_mutex_trylock(&self->mutex) == 0) { + atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + return 1; + } + Py_BEGIN_ALLOW_THREADS pthread_mutex_lock(&self->mutex); Py_END_ALLOW_THREADS From 4ee0f714c31f95c7ed3ff899c52a6cf8d6715600 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 19 Mar 2026 16:23:07 +0000 Subject: [PATCH 13/45] WIP benchmark --- mypyc/test-data/run-threading.test | 110 +++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index d46abd4cfa070..ba055a630a66c 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -78,3 +78,113 @@ def test_release_unlocked() -> None: pass else: assert False, "expected RuntimeError" + +[case testLockPerf_librt_experimental_benchmark] +import threading +from librt.threading import Lock + +N = 5000000 + +def bench_librt_acquire_release() -> None: + lock = Lock() + n = N + i = 0 + while i < n: + lock.acquire() + lock.release() + i += 1 + +def bench_librt_with() -> None: + lock = Lock() + n = N + i = 0 + while i < n: + with lock: + pass + i += 1 + +def bench_stdlib_acquire_release() -> None: + lock = threading.Lock() + n = N + i = 0 + while i < n: + lock.acquire() + lock.release() + i += 1 + +def bench_stdlib_with() -> None: + lock = threading.Lock() + n = N + i = 0 + while i < n: + with lock: + pass + i += 1 + +[file driver.py] +import time +import threading +import native + +N = native.N +REPEAT = 5 + +def bench_interp_acquire_release() -> None: + lock = threading.Lock() + n = N + i = 0 + while i < n: + lock.acquire() + lock.release() + i += 1 + +def bench_interp_with() -> None: + lock = threading.Lock() + n = N + i = 0 + while i < n: + with lock: + pass + i += 1 + +def best_of(func: object, repeat: int) -> float: + best = float("inf") + for r in range(repeat): + t0 = time.perf_counter() + func() # type: ignore[operator] + dt = time.perf_counter() - t0 + best = min(best, dt) + return best + +def ns_per(seconds: float) -> float: + return seconds / N * 1e9 + +def run() -> None: + print(f"Lock acquire/release and with, {N} iterations, best of {REPEAT}") + print() + + t1 = best_of(native.bench_librt_acquire_release, REPEAT) + print(f" librt Lock acquire/release (compiled): {ns_per(t1):.1f} ns") + + t2 = best_of(native.bench_librt_with, REPEAT) + print(f" librt Lock with (compiled): {ns_per(t2):.1f} ns") + + t3 = best_of(native.bench_stdlib_acquire_release, REPEAT) + print(f" stdlib Lock acquire/release (compiled): {ns_per(t3):.1f} ns") + + t4 = best_of(native.bench_stdlib_with, REPEAT) + print(f" stdlib Lock with (compiled): {ns_per(t4):.1f} ns") + + t5 = best_of(bench_interp_acquire_release, REPEAT) + print(f" stdlib Lock acquire/release (interp): {ns_per(t5):.1f} ns") + + t6 = best_of(bench_interp_with, REPEAT) + print(f" stdlib Lock with (interp): {ns_per(t6):.1f} ns") + + print() + print(f" acquire/release: librt vs stdlib compiled: {t3/t1:.2f}x") + print(f" acquire/release: librt vs interp: {t5/t1:.2f}x") + print(f" with: librt vs stdlib compiled: {t4/t2:.2f}x") + print(f" with: librt vs interp: {t6/t2:.2f}x") + +run() From 8c54de506c08b7e4690daa0cbb436ea65876e57a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 23 Mar 2026 14:25:32 +0000 Subject: [PATCH 14/45] Fix double init --- mypyc/lib-rt/threading/librt_threading.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 39853fe836a61..e36d5bfe5091c 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -159,9 +159,6 @@ Lock_new(PyTypeObject *type, PyObject *args, PyObject *kwds) } LockObject *self = (LockObject *)type->tp_alloc(type, 0); - if (self != NULL) { - Lock_init_internal(self); - } return (PyObject *)self; } From d5c430650790db8feb6b1f09e5927f44cf85e256 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 23 Mar 2026 14:32:01 +0000 Subject: [PATCH 15/45] Support keyword argument --- mypyc/lib-rt/threading/librt_threading.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index e36d5bfe5091c..ae51c0ef323e8 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -189,17 +189,32 @@ Lock_dealloc(LockObject *self) } static PyObject * -Lock_acquire(LockObject *self, PyObject *const *args, Py_ssize_t nargs) +Lock_acquire(LockObject *self, PyObject *const *args, Py_ssize_t nargs, + PyObject *kwnames) { int blocking = 1; - if (nargs > 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); @@ -238,7 +253,7 @@ Lock_exit(LockObject *self, PyObject *const *args, Py_ssize_t nargs) } static PyMethodDef Lock_methods[] = { - {"acquire", (PyCFunction)Lock_acquire, METH_FASTCALL, + {"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, From ce9b478141207e2d5528f88ef4eb036709c3886f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 27 Apr 2026 14:11:56 +0100 Subject: [PATCH 16/45] Use the more recent approach for defining librt submodules --- mypyc/lib-rt/threading/librt_threading.h | 56 -------------------- mypyc/lib-rt/threading/librt_threading_api.c | 51 ++++++++++++++++++ mypyc/lib-rt/threading/librt_threading_api.h | 23 ++++++++ 3 files changed, 74 insertions(+), 56 deletions(-) create mode 100644 mypyc/lib-rt/threading/librt_threading_api.c create mode 100644 mypyc/lib-rt/threading/librt_threading_api.h diff --git a/mypyc/lib-rt/threading/librt_threading.h b/mypyc/lib-rt/threading/librt_threading.h index 784a82108c4fe..9c56bfc01f8ff 100644 --- a/mypyc/lib-rt/threading/librt_threading.h +++ b/mypyc/lib-rt/threading/librt_threading.h @@ -1,66 +1,10 @@ #ifndef LIBRT_THREADING_H #define LIBRT_THREADING_H -#ifndef MYPYC_EXPERIMENTAL - -static int -import_librt_threading(void) -{ - // All librt.threading features are experimental for now, so don't set up the API here - return 0; -} - -#else // MYPYC_EXPERIMENTAL - #include #define LIBRT_THREADING_ABI_VERSION 1 #define LIBRT_THREADING_API_VERSION 2 #define LIBRT_THREADING_API_LEN 7 -static void *LibRTThreading_API[LIBRT_THREADING_API_LEN]; - -#define LibRTThreading_ABIVersion (*(int (*)(void)) LibRTThreading_API[0]) -#define LibRTThreading_APIVersion (*(int (*)(void)) LibRTThreading_API[1]) -#define LibRTThreading_Lock_type_internal (*(PyTypeObject* (*)(void)) LibRTThreading_API[2]) -#define LibRTThreading_Lock_new_internal (*(PyObject* (*)(void)) LibRTThreading_API[3]) -#define LibRTThreading_Lock_acquire_internal (*(char (*)(PyObject *self)) LibRTThreading_API[4]) -#define LibRTThreading_Lock_release_internal (*(char (*)(PyObject *self)) LibRTThreading_API[5]) -#define LibRTThreading_Lock_locked_internal (*(char (*)(PyObject *self)) LibRTThreading_API[6]) - -static int -import_librt_threading(void) -{ - PyObject *mod = PyImport_ImportModule("librt.threading"); - if (mod == NULL) - return -1; - Py_DECREF(mod); // we import just for the side effect of making the below work. - void *capsule = PyCapsule_Import("librt.threading._C_API", 0); - if (capsule == NULL) - return -1; - memcpy(LibRTThreading_API, capsule, sizeof(LibRTThreading_API)); - if (LibRTThreading_ABIVersion() != LIBRT_THREADING_ABI_VERSION) { - char err[128]; - snprintf(err, sizeof(err), "ABI version conflict for librt.threading, expected %d, found %d", - LIBRT_THREADING_ABI_VERSION, - LibRTThreading_ABIVersion() - ); - PyErr_SetString(PyExc_ValueError, err); - return -1; - } - if (LibRTThreading_APIVersion() < LIBRT_THREADING_API_VERSION) { - char err[128]; - snprintf(err, sizeof(err), - "API version conflict for librt.threading, expected %d or newer, found %d (hint: upgrade librt)", - LIBRT_THREADING_API_VERSION, - LibRTThreading_APIVersion() - ); - PyErr_SetString(PyExc_ValueError, err); - return -1; - } - return 0; -} - -#endif // MYPYC_EXPERIMENTAL - #endif // LIBRT_THREADING_H diff --git a/mypyc/lib-rt/threading/librt_threading_api.c b/mypyc/lib-rt/threading/librt_threading_api.c new file mode 100644 index 0000000000000..a93af80b9d773 --- /dev/null +++ b/mypyc/lib-rt/threading/librt_threading_api.c @@ -0,0 +1,51 @@ +#include "librt_threading_api.h" + +#ifdef MYPYC_EXPERIMENTAL + +void *LibRTThreading_API[LIBRT_THREADING_API_LEN] = {0}; + +#endif + +int +import_librt_threading(void) +{ +#ifndef MYPYC_EXPERIMENTAL + return 0; +#else + PyObject *mod = PyImport_ImportModule("librt.threading"); + if (mod == NULL) + return -1; + Py_DECREF(mod); // we import just for the side effect of making the below work. + void **capsule = (void **)PyCapsule_Import("librt.threading._C_API", 0); + if (capsule == NULL) + return -1; + + // Only after version validation succeeds can we safely copy the full table. + int (*abi_version)(void) = (int (*)(void))capsule[0]; + int (*api_version)(void) = (int (*)(void))capsule[1]; + if (abi_version() != LIBRT_THREADING_ABI_VERSION) { + char err[128]; + snprintf(err, sizeof(err), "ABI version conflict for librt.threading, expected %d, found %d", + LIBRT_THREADING_ABI_VERSION, + abi_version() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + if (api_version() < LIBRT_THREADING_API_VERSION) { + char err[128]; + snprintf(err, sizeof(err), + "API version conflict for librt.threading, expected %d or newer, found %d (hint: upgrade librt)", + LIBRT_THREADING_API_VERSION, + api_version() + ); + PyErr_SetString(PyExc_ValueError, err); + return -1; + } + // Provider API version is >= our expected version, which (by the API + // compatibility contract) means it has at least LIBRT_THREADING_API_LEN + // entries, so this copy is safe. + memcpy(LibRTThreading_API, capsule, sizeof(LibRTThreading_API)); + return 0; +#endif +} diff --git a/mypyc/lib-rt/threading/librt_threading_api.h b/mypyc/lib-rt/threading/librt_threading_api.h new file mode 100644 index 0000000000000..2fc00d9433fb1 --- /dev/null +++ b/mypyc/lib-rt/threading/librt_threading_api.h @@ -0,0 +1,23 @@ +#ifndef LIBRT_THREADING_API_H +#define LIBRT_THREADING_API_H + +#include "librt_threading.h" + +int +import_librt_threading(void); + +#ifdef MYPYC_EXPERIMENTAL + +extern void *LibRTThreading_API[LIBRT_THREADING_API_LEN]; + +#define LibRTThreading_ABIVersion (*(int (*)(void)) LibRTThreading_API[0]) +#define LibRTThreading_APIVersion (*(int (*)(void)) LibRTThreading_API[1]) +#define LibRTThreading_Lock_type_internal (*(PyTypeObject* (*)(void)) LibRTThreading_API[2]) +#define LibRTThreading_Lock_new_internal (*(PyObject* (*)(void)) LibRTThreading_API[3]) +#define LibRTThreading_Lock_acquire_internal (*(char (*)(PyObject *self)) LibRTThreading_API[4]) +#define LibRTThreading_Lock_release_internal (*(char (*)(PyObject *self)) LibRTThreading_API[5]) +#define LibRTThreading_Lock_locked_internal (*(char (*)(PyObject *self)) LibRTThreading_API[6]) + +#endif // MYPYC_EXPERIMENTAL + +#endif // LIBRT_THREADING_API_H From 030d09bde071b8ef6879d04800aab240c0bd82b4 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 27 Apr 2026 15:09:20 +0100 Subject: [PATCH 17/45] Use Python 3.14 PyMutex on 3.14+ --- mypyc/lib-rt/threading/librt_threading.c | 69 +++++++++++++++++++----- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index ae51c0ef323e8..183cd9474d68b 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -5,16 +5,27 @@ #include "librt_threading.h" #include "mypyc_util.h" -// Select lock backend: -// LOCK_BACKEND_SRWLOCK - Windows Slim Reader/Writer Lock -// LOCK_BACKEND_PTHREAD - POSIX (macOS, Linux, and other Unix-like systems) -#if defined(_WIN32) +#if CPY_3_14_FEATURES + +// Python 3.14+: Use PyMutex (1-byte atomic lock with parking lot) +#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 + #else + +// Python <3.14 on POSIX: Use pthread mutex #define LOCK_BACKEND_PTHREAD #include #include + #endif // @@ -22,18 +33,29 @@ // // A fast mutex lock for use from mypyc-compiled code. // -// On Windows, this uses SRWLOCK (Slim Reader/Writer Lock), a lightweight -// kernel primitive. A separate volatile flag tracks the locked state. +// On Python 3.14+, 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 older Python with Windows, this uses SRWLOCK (Slim Reader/Writer Lock), +// a lightweight kernel primitive. A separate volatile flag tracks the locked state. // -// On POSIX systems (macOS, Linux, etc.), this uses pthread_mutex with -// a separate atomic flag for locked() and release-unlocked-lock detection. +// On older Python with POSIX systems (macOS, Linux, etc.), this uses pthread_mutex +// with a separate atomic flag for locked() and release-unlocked-lock detection. // #ifdef MYPYC_EXPERIMENTAL // ---------- Platform-specific lock state ---------- -#ifdef LOCK_BACKEND_SRWLOCK +#if CPY_3_14_FEATURES + +typedef struct { + PyObject_HEAD + PyMutex mutex; +} LockObject; + +#elif defined(LOCK_BACKEND_SRWLOCK) typedef struct { PyObject_HEAD @@ -56,7 +78,9 @@ typedef struct { static inline void Lock_init_internal(LockObject *self) { -#ifdef LOCK_BACKEND_SRWLOCK +#if CPY_3_14_FEATURES + self->mutex = (PyMutex){0}; +#elif defined(LOCK_BACKEND_SRWLOCK) InitializeSRWLock(&self->lock); self->locked = 0; #else @@ -70,7 +94,17 @@ Lock_init_internal(LockObject *self) static int Lock_acquire_impl(LockObject *self, int blocking) { -#ifdef LOCK_BACKEND_SRWLOCK +#if CPY_3_14_FEATURES + if (!blocking) { + return PyMutex_LockFast(&self->mutex); + } + if (PyMutex_LockFast(&self->mutex)) { + return 1; + } + _PyMutex_LockTimed(&self->mutex, -1, _PY_LOCK_DETACH); + return 1; + +#elif defined(LOCK_BACKEND_SRWLOCK) if (!blocking) { if (TryAcquireSRWLockExclusive(&self->lock)) { InterlockedExchange(&self->locked, 1); @@ -120,7 +154,14 @@ Lock_acquire_impl(LockObject *self, int blocking) static int Lock_release_impl(LockObject *self) { -#ifdef LOCK_BACKEND_SRWLOCK +#if CPY_3_14_FEATURES + if (!PyMutex_IsLocked(&self->mutex)) { + return -1; + } + PyMutex_Unlock(&self->mutex); + return 0; + +#elif defined(LOCK_BACKEND_SRWLOCK) if (!InterlockedExchange(&self->locked, 0)) { return -1; } @@ -139,7 +180,9 @@ Lock_release_impl(LockObject *self) static inline int Lock_is_locked(LockObject *self) { -#ifdef LOCK_BACKEND_SRWLOCK +#if CPY_3_14_FEATURES + return PyMutex_IsLocked(&self->mutex); +#elif defined(LOCK_BACKEND_SRWLOCK) return InterlockedCompareExchange(&self->locked, 0, 0) != 0; #else return atomic_load_explicit(&self->locked, memory_order_relaxed) != 0; From 585ef10354949d810ac712308752f1bf10d6edbc Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 27 Apr 2026 16:16:23 +0100 Subject: [PATCH 18/45] Misc updates --- mypyc/irbuild/statement.py | 3 +-- mypyc/lib-rt/threading/librt_threading.c | 15 ++++++++++++++- mypyc/lib-rt/threading/librt_threading.h | 4 ++-- mypyc/lib-rt/threading/librt_threading_api.h | 1 + mypyc/primitives/librt_threading_ops.py | 11 +++++++++++ mypyc/test-data/irbuild-threading.test | 13 +++++++++++++ 6 files changed, 42 insertions(+), 5 deletions(-) diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index c73c278e2493f..cb2155283b057 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -128,6 +128,7 @@ type_op, yield_from_except_op, ) +from mypyc.primitives.librt_threading_ops import lock_acquire_op, lock_release_op from .match import MatchVisitor @@ -1193,8 +1194,6 @@ def transform_with_lock( Lock.__exit__ never suppresses exceptions, so we don't need the full PEP 343 try/except/finally machinery. """ - from mypyc.primitives.librt_threading_ops import lock_acquire_op, lock_release_op - # __enter__: acquire the lock value = builder.primitive_op(lock_acquire_op, [mgr_v], line) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 183cd9474d68b..a5d7c8749b193 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -7,7 +7,9 @@ #if CPY_3_14_FEATURES -// Python 3.14+: Use PyMutex (1-byte atomic lock with parking lot) +// Python 3.14+: Use PyMutex (1-byte atomic lock with parking lot). +// PyMutex_LockFast, _PyMutex_LockTimed, and _PY_LOCK_DETACH are internal +// CPython APIs that might change across minor releases. #ifndef Py_BUILD_CORE #define Py_BUILD_CORE #endif @@ -155,6 +157,8 @@ static int Lock_release_impl(LockObject *self) { #if CPY_3_14_FEATURES + // Note: check-then-unlock is not atomic, but this matches CPython's + // threading.Lock semantics. Only the owning thread should call release(). if (!PyMutex_IsLocked(&self->mutex)) { return -1; } @@ -346,6 +350,14 @@ Lock_acquire_internal(PyObject *self) { return (char)result; } +// Acquire the lock with explicit blocking arg, for use from compiled code. +// Returns true if acquired, false otherwise. +static char +Lock_acquire_blocking_internal(PyObject *self, char blocking) { + int result = Lock_acquire_impl((LockObject *)self, blocking); + 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 @@ -403,6 +415,7 @@ librt_threading_module_exec(PyObject *m) (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) { diff --git a/mypyc/lib-rt/threading/librt_threading.h b/mypyc/lib-rt/threading/librt_threading.h index 9c56bfc01f8ff..a22284f3fdbcd 100644 --- a/mypyc/lib-rt/threading/librt_threading.h +++ b/mypyc/lib-rt/threading/librt_threading.h @@ -4,7 +4,7 @@ #include #define LIBRT_THREADING_ABI_VERSION 1 -#define LIBRT_THREADING_API_VERSION 2 -#define LIBRT_THREADING_API_LEN 7 +#define LIBRT_THREADING_API_VERSION 1 +#define LIBRT_THREADING_API_LEN 8 #endif // LIBRT_THREADING_H diff --git a/mypyc/lib-rt/threading/librt_threading_api.h b/mypyc/lib-rt/threading/librt_threading_api.h index 2fc00d9433fb1..4df15b889785b 100644 --- a/mypyc/lib-rt/threading/librt_threading_api.h +++ b/mypyc/lib-rt/threading/librt_threading_api.h @@ -17,6 +17,7 @@ extern void *LibRTThreading_API[LIBRT_THREADING_API_LEN]; #define LibRTThreading_Lock_acquire_internal (*(char (*)(PyObject *self)) LibRTThreading_API[4]) #define LibRTThreading_Lock_release_internal (*(char (*)(PyObject *self)) LibRTThreading_API[5]) #define LibRTThreading_Lock_locked_internal (*(char (*)(PyObject *self)) LibRTThreading_API[6]) +#define LibRTThreading_Lock_acquire_blocking_internal (*(char (*)(PyObject *self, char blocking)) LibRTThreading_API[7]) #endif // MYPYC_EXPERIMENTAL diff --git a/mypyc/primitives/librt_threading_ops.py b/mypyc/primitives/librt_threading_ops.py index b7d863772490a..4b692e696c167 100644 --- a/mypyc/primitives/librt_threading_ops.py +++ b/mypyc/primitives/librt_threading_ops.py @@ -25,6 +25,17 @@ dependencies=[LIBRT_THREADING], ) +# Lock.acquire(blocking) -- acquire with explicit blocking argument +method_op( + name="acquire", + arg_types=[lock_rprimitive, bool_rprimitive], + return_type=bool_rprimitive, + c_function_name="LibRTThreading_Lock_acquire_blocking_internal", + error_kind=ERR_NEVER, + experimental=True, + dependencies=[LIBRT_THREADING], +) + # Lock.release() lock_release_op = method_op( name="release", diff --git a/mypyc/test-data/irbuild-threading.test b/mypyc/test-data/irbuild-threading.test index bbcb61f81fbc0..964c259d0906d 100644 --- a/mypyc/test-data/irbuild-threading.test +++ b/mypyc/test-data/irbuild-threading.test @@ -56,6 +56,19 @@ L0: r2 = LibRTThreading_Lock_release_internal(lk) return 1 +[case testLockAcquireBlocking_experimental] +from librt.threading import Lock + +def lock_acquire_blocking(lk: Lock, b: bool) -> bool: + return lk.acquire(b) +[out] +def lock_acquire_blocking(lk, b): + lk :: librt.threading.Lock + b, r0 :: bool +L0: + r0 = LibRTThreading_Lock_acquire_blocking_internal(lk, b) + return r0 + [case testLockWith_experimental] from librt.threading import Lock From 00811f5078a22e601b2bea88757f548a9e5c339d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 1 Jun 2026 17:03:54 +0100 Subject: [PATCH 19/45] Revert "WIP benchmark" This reverts commit 9b2bc222edf9780fb10a0f108ab8b098b78a2c84. --- mypyc/test-data/run-threading.test | 110 ----------------------------- 1 file changed, 110 deletions(-) diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index ba055a630a66c..d46abd4cfa070 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -78,113 +78,3 @@ def test_release_unlocked() -> None: pass else: assert False, "expected RuntimeError" - -[case testLockPerf_librt_experimental_benchmark] -import threading -from librt.threading import Lock - -N = 5000000 - -def bench_librt_acquire_release() -> None: - lock = Lock() - n = N - i = 0 - while i < n: - lock.acquire() - lock.release() - i += 1 - -def bench_librt_with() -> None: - lock = Lock() - n = N - i = 0 - while i < n: - with lock: - pass - i += 1 - -def bench_stdlib_acquire_release() -> None: - lock = threading.Lock() - n = N - i = 0 - while i < n: - lock.acquire() - lock.release() - i += 1 - -def bench_stdlib_with() -> None: - lock = threading.Lock() - n = N - i = 0 - while i < n: - with lock: - pass - i += 1 - -[file driver.py] -import time -import threading -import native - -N = native.N -REPEAT = 5 - -def bench_interp_acquire_release() -> None: - lock = threading.Lock() - n = N - i = 0 - while i < n: - lock.acquire() - lock.release() - i += 1 - -def bench_interp_with() -> None: - lock = threading.Lock() - n = N - i = 0 - while i < n: - with lock: - pass - i += 1 - -def best_of(func: object, repeat: int) -> float: - best = float("inf") - for r in range(repeat): - t0 = time.perf_counter() - func() # type: ignore[operator] - dt = time.perf_counter() - t0 - best = min(best, dt) - return best - -def ns_per(seconds: float) -> float: - return seconds / N * 1e9 - -def run() -> None: - print(f"Lock acquire/release and with, {N} iterations, best of {REPEAT}") - print() - - t1 = best_of(native.bench_librt_acquire_release, REPEAT) - print(f" librt Lock acquire/release (compiled): {ns_per(t1):.1f} ns") - - t2 = best_of(native.bench_librt_with, REPEAT) - print(f" librt Lock with (compiled): {ns_per(t2):.1f} ns") - - t3 = best_of(native.bench_stdlib_acquire_release, REPEAT) - print(f" stdlib Lock acquire/release (compiled): {ns_per(t3):.1f} ns") - - t4 = best_of(native.bench_stdlib_with, REPEAT) - print(f" stdlib Lock with (compiled): {ns_per(t4):.1f} ns") - - t5 = best_of(bench_interp_acquire_release, REPEAT) - print(f" stdlib Lock acquire/release (interp): {ns_per(t5):.1f} ns") - - t6 = best_of(bench_interp_with, REPEAT) - print(f" stdlib Lock with (interp): {ns_per(t6):.1f} ns") - - print() - print(f" acquire/release: librt vs stdlib compiled: {t3/t1:.2f}x") - print(f" acquire/release: librt vs interp: {t5/t1:.2f}x") - print(f" with: librt vs stdlib compiled: {t4/t2:.2f}x") - print(f" with: librt vs interp: {t6/t2:.2f}x") - -run() From 7a4e7078d995987b39c8f8d7161f01ff0f6b2438 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 2 Jun 2026 18:38:23 +0100 Subject: [PATCH 20/45] Allow release from a different thread on posix --- mypyc/lib-rt/threading/librt_threading.c | 64 ++++++++++++++++++------ mypyc/test-data/run-threading.test | 26 ++++++++++ 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index a5d7c8749b193..7da85db263cea 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -42,8 +42,12 @@ // On older Python with Windows, this uses SRWLOCK (Slim Reader/Writer Lock), // a lightweight kernel primitive. A separate volatile flag tracks the locked state. // -// On older Python with POSIX systems (macOS, Linux, etc.), this uses pthread_mutex -// with a separate atomic flag for locked() and release-unlocked-lock detection. +// On older Python with POSIX systems (macOS, Linux, etc.), this uses a +// pthread mutex + condition variable guarding a `locked` flag (mirroring +// CPython's portable POSIX lock). The mutex 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 one that acquired the lock, matching +// threading.Lock semantics. // #ifdef MYPYC_EXPERIMENTAL @@ -69,8 +73,15 @@ typedef struct { typedef struct { PyObject_HEAD - pthread_mutex_t mutex; - _Atomic int locked; // 0=unlocked, 1=locked (for locked() and error checking) + // 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; + _Atomic int locked; // 0=unlocked, 1=locked; protected by `mut` } LockObject; #endif @@ -86,7 +97,8 @@ Lock_init_internal(LockObject *self) InitializeSRWLock(&self->lock); self->locked = 0; #else - pthread_mutex_init(&self->mutex, NULL); + pthread_mutex_init(&self->mut, NULL); + pthread_cond_init(&self->lock_released, NULL); atomic_store_explicit(&self->locked, 0, memory_order_relaxed); #endif } @@ -129,25 +141,39 @@ Lock_acquire_impl(LockObject *self, int blocking) return 1; #else // POSIX 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(). if (!blocking) { - if (pthread_mutex_trylock(&self->mutex) == 0) { + pthread_mutex_lock(&self->mut); + if (!atomic_load_explicit(&self->locked, memory_order_relaxed)) { atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + pthread_mutex_unlock(&self->mut); return 1; } + pthread_mutex_unlock(&self->mut); return 0; } - // Fast path: try non-blocking acquire first to avoid GIL release/reacquire - // overhead in the common uncontended case. - if (pthread_mutex_trylock(&self->mutex) == 0) { + // Fast path: grab the lock without releasing the GIL if it is free. + pthread_mutex_lock(&self->mut); + if (!atomic_load_explicit(&self->locked, memory_order_relaxed)) { atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + pthread_mutex_unlock(&self->mut); return 1; } + pthread_mutex_unlock(&self->mut); + // Slow path: wait for the lock to be released, with the GIL dropped so + // other Python threads can run (and release the lock). Py_BEGIN_ALLOW_THREADS - pthread_mutex_lock(&self->mutex); - Py_END_ALLOW_THREADS + pthread_mutex_lock(&self->mut); + while (atomic_load_explicit(&self->locked, memory_order_relaxed)) { + pthread_cond_wait(&self->lock_released, &self->mut); + } atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + pthread_mutex_unlock(&self->mut); + Py_END_ALLOW_THREADS return 1; #endif } @@ -173,10 +199,16 @@ Lock_release_impl(LockObject *self) return 0; #else // POSIX fallback - if (!atomic_exchange_explicit(&self->locked, 0, memory_order_relaxed)) { + pthread_mutex_lock(&self->mut); + if (!atomic_load_explicit(&self->locked, memory_order_relaxed)) { + pthread_mutex_unlock(&self->mut); return -1; } - pthread_mutex_unlock(&self->mutex); + atomic_store_explicit(&self->locked, 0, memory_order_relaxed); + // 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 } @@ -230,7 +262,11 @@ static void Lock_dealloc(LockObject *self) { #ifdef LOCK_BACKEND_PTHREAD - pthread_mutex_destroy(&self->mutex); + // `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); } diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index d46abd4cfa070..2a0d408df071d 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -45,6 +45,32 @@ def test_contention() -> None: t.join() assert counter[0] == n_threads * n_increments +def test_cross_thread_release() -> None: + # threading.Lock is unowned: a lock acquired on one thread may be + # released from another. A thread blocked in acquire() must then wake up. + import threading + lock = Lock() + assert lock.acquire() + + started = threading.Event() + released = threading.Event() + + def releaser() -> None: + started.set() + # Hand the lock off from a different thread than the acquirer. + lock.release() + released.set() + + t = threading.Thread(target=releaser) + t.start() + started.wait() + # Blocks until releaser releases the lock on the other thread. + assert lock.acquire() + t.join() + assert released.is_set() + lock.release() + assert not lock.locked() + def test_context_manager_exception() -> None: lock = Lock() try: From 5ce8750871e864c1b69418ff97c6da65ed1d0b4b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 2 Jun 2026 18:56:32 +0100 Subject: [PATCH 21/45] Add sem_t implementation --- mypyc/lib-rt/threading/librt_threading.c | 117 ++++++++++++++++++++--- 1 file changed, 106 insertions(+), 11 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 7da85db263cea..42885e9d0f7f3 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -23,10 +23,27 @@ #else -// Python <3.14 on POSIX: Use pthread mutex +// Python <3.14 on POSIX. +// +// Prefer a POSIX unnamed semaphore when the platform supports it well, 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. +#include +#if defined(_POSIX_SEMAPHORES) && (_POSIX_SEMAPHORES + 0) != -1 && \ + (defined(HAVE_SEM_TIMEDWAIT) || defined(HAVE_SEM_CLOCKWAIT)) +#define LOCK_BACKEND_SEM +#include +#include +#include +#else #define LOCK_BACKEND_PTHREAD #include #include +#endif #endif @@ -42,12 +59,20 @@ // On older Python with Windows, this uses SRWLOCK (Slim Reader/Writer Lock), // a lightweight kernel primitive. A separate volatile flag tracks the locked state. // -// On older Python with POSIX systems (macOS, Linux, etc.), this uses a -// pthread mutex + condition variable guarding a `locked` flag (mirroring -// CPython's portable POSIX lock). The mutex 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 one that acquired the lock, matching -// threading.Lock semantics. +// On older Python 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), 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), 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. // #ifdef MYPYC_EXPERIMENTAL @@ -69,7 +94,19 @@ typedef struct { volatile long locked; // 0=unlocked, 1=locked (for locked() and error checking) } LockObject; -#else // POSIX fallback +#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. + _Atomic int locked; +} LockObject; + +#else // pthread mutex + condvar fallback typedef struct { PyObject_HEAD @@ -96,6 +133,9 @@ Lock_init_internal(LockObject *self) #elif defined(LOCK_BACKEND_SRWLOCK) InitializeSRWLock(&self->lock); self->locked = 0; +#elif defined(LOCK_BACKEND_SEM) + sem_init(&self->sem, 0, 1); + atomic_store_explicit(&self->locked, 0, memory_order_relaxed); #else pthread_mutex_init(&self->mut, NULL); pthread_cond_init(&self->lock_released, NULL); @@ -140,7 +180,50 @@ Lock_acquire_impl(LockObject *self, int blocking) InterlockedExchange(&self->locked, 1); return 1; -#else // POSIX fallback +#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. + if (!blocking) { + int status; + do { + status = sem_trywait(&self->sem); + } while (status == -1 && errno == EINTR); + if (status == 0) { + atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + return 1; + } + return 0; // EAGAIN: already held + } + + // Fast path: try non-blocking acquire first to avoid GIL release/reacquire + // overhead in the common uncontended case. + { + int status; + do { + status = sem_trywait(&self->sem); + } while (status == -1 && errno == EINTR); + if (status == 0) { + atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + return 1; + } + } + + // Slow path: block with the GIL dropped so other Python threads can run + // (and release the lock). Retry on EINTR (signal). + Py_BEGIN_ALLOW_THREADS + { + int status; + do { + status = sem_wait(&self->sem); + } while (status == -1 && errno == EINTR); + } + Py_END_ALLOW_THREADS + atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + 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(). @@ -198,7 +281,17 @@ Lock_release_impl(LockObject *self) ReleaseSRWLockExclusive(&self->lock); return 0; -#else // POSIX fallback +#elif defined(LOCK_BACKEND_SEM) + // Atomically clear the flag; only the caller that observes the previous + // value as 1 actually owns the release and posts the semaphore. This + // prevents a double release from over-incrementing the semaphore. + if (!atomic_exchange_explicit(&self->locked, 0, memory_order_relaxed)) { + return -1; + } + sem_post(&self->sem); + return 0; + +#else // pthread mutex + condvar fallback pthread_mutex_lock(&self->mut); if (!atomic_load_explicit(&self->locked, memory_order_relaxed)) { pthread_mutex_unlock(&self->mut); @@ -261,7 +354,9 @@ Lock_init(LockObject *self, PyObject *args, PyObject *kwds) static void Lock_dealloc(LockObject *self) { -#ifdef LOCK_BACKEND_PTHREAD +#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. From dca25a4210b281d4dd412f581581c372878217dd Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 09:55:45 +0100 Subject: [PATCH 22/45] Allow Lock to be released on different thread in Windows --- mypyc/lib-rt/threading/librt_threading.c | 74 +++++++++++++++++++----- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 42885e9d0f7f3..1e2ba5a1d28bb 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -56,8 +56,13 @@ // backed by a parking lot for contended waits. PyMutex automatically // releases the GIL when blocking. // -// On older Python with Windows, this uses SRWLOCK (Slim Reader/Writer Lock), -// a lightweight kernel primitive. A separate volatile flag tracks the locked state. +// On older Python 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 older Python with POSIX systems, there are two backends, both of which // allow release() from a thread other than the one that acquired the lock @@ -90,8 +95,16 @@ typedef struct { typedef struct { PyObject_HEAD - SRWLOCK lock; - volatile long locked; // 0=unlocked, 1=locked (for locked() and error checking) + // 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) @@ -131,7 +144,8 @@ Lock_init_internal(LockObject *self) #if CPY_3_14_FEATURES self->mutex = (PyMutex){0}; #elif defined(LOCK_BACKEND_SRWLOCK) - InitializeSRWLock(&self->lock); + InitializeSRWLock(&self->srw); + InitializeConditionVariable(&self->lock_released); self->locked = 0; #elif defined(LOCK_BACKEND_SEM) sem_init(&self->sem, 0, 1); @@ -159,25 +173,41 @@ Lock_acquire_impl(LockObject *self, int blocking) 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(). if (!blocking) { - if (TryAcquireSRWLockExclusive(&self->lock)) { - InterlockedExchange(&self->locked, 1); + AcquireSRWLockExclusive(&self->srw); + if (!self->locked) { + self->locked = 1; + ReleaseSRWLockExclusive(&self->srw); return 1; } + ReleaseSRWLockExclusive(&self->srw); return 0; } - // Fast path: try non-blocking acquire first to avoid GIL release/reacquire - // overhead in the common uncontended case. - if (TryAcquireSRWLockExclusive(&self->lock)) { - InterlockedExchange(&self->locked, 1); + // Fast path: grab the lock without releasing the GIL if it is free. + AcquireSRWLockExclusive(&self->srw); + if (!self->locked) { + self->locked = 1; + ReleaseSRWLockExclusive(&self->srw); return 1; } + ReleaseSRWLockExclusive(&self->srw); + // 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->lock); + AcquireSRWLockExclusive(&self->srw); + while (self->locked) { + SleepConditionVariableSRW(&self->lock_released, &self->srw, INFINITE, 0); + } + self->locked = 1; + ReleaseSRWLockExclusive(&self->srw); Py_END_ALLOW_THREADS - InterlockedExchange(&self->locked, 1); return 1; #elif defined(LOCK_BACKEND_SEM) @@ -275,10 +305,16 @@ Lock_release_impl(LockObject *self) return 0; #elif defined(LOCK_BACKEND_SRWLOCK) - if (!InterlockedExchange(&self->locked, 0)) { + AcquireSRWLockExclusive(&self->srw); + if (!self->locked) { + ReleaseSRWLockExclusive(&self->srw); return -1; } - ReleaseSRWLockExclusive(&self->lock); + 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) @@ -312,7 +348,13 @@ Lock_is_locked(LockObject *self) #if CPY_3_14_FEATURES return PyMutex_IsLocked(&self->mutex); #elif defined(LOCK_BACKEND_SRWLOCK) - return InterlockedCompareExchange(&self->locked, 0, 0) != 0; + // 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; #else return atomic_load_explicit(&self->locked, memory_order_relaxed) != 0; #endif From 002b31d69b04e80b7b4c08c090b240f144992191 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 10:51:08 +0100 Subject: [PATCH 23/45] Reapply "WIP benchmark" This reverts commit ce077a4726d47bd494bd9b09e3210b2c96979527. --- mypyc/test-data/run-threading.test | 110 +++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index 2a0d408df071d..3c9b362d97d62 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -104,3 +104,113 @@ def test_release_unlocked() -> None: pass else: assert False, "expected RuntimeError" + +[case testLockPerf_librt_experimental_benchmark] +import threading +from librt.threading import Lock + +N = 5000000 + +def bench_librt_acquire_release() -> None: + lock = Lock() + n = N + i = 0 + while i < n: + lock.acquire() + lock.release() + i += 1 + +def bench_librt_with() -> None: + lock = Lock() + n = N + i = 0 + while i < n: + with lock: + pass + i += 1 + +def bench_stdlib_acquire_release() -> None: + lock = threading.Lock() + n = N + i = 0 + while i < n: + lock.acquire() + lock.release() + i += 1 + +def bench_stdlib_with() -> None: + lock = threading.Lock() + n = N + i = 0 + while i < n: + with lock: + pass + i += 1 + +[file driver.py] +import time +import threading +import native + +N = native.N +REPEAT = 5 + +def bench_interp_acquire_release() -> None: + lock = threading.Lock() + n = N + i = 0 + while i < n: + lock.acquire() + lock.release() + i += 1 + +def bench_interp_with() -> None: + lock = threading.Lock() + n = N + i = 0 + while i < n: + with lock: + pass + i += 1 + +def best_of(func: object, repeat: int) -> float: + best = float("inf") + for r in range(repeat): + t0 = time.perf_counter() + func() # type: ignore[operator] + dt = time.perf_counter() - t0 + best = min(best, dt) + return best + +def ns_per(seconds: float) -> float: + return seconds / N * 1e9 + +def run() -> None: + print(f"Lock acquire/release and with, {N} iterations, best of {REPEAT}") + print() + + t1 = best_of(native.bench_librt_acquire_release, REPEAT) + print(f" librt Lock acquire/release (compiled): {ns_per(t1):.1f} ns") + + t2 = best_of(native.bench_librt_with, REPEAT) + print(f" librt Lock with (compiled): {ns_per(t2):.1f} ns") + + t3 = best_of(native.bench_stdlib_acquire_release, REPEAT) + print(f" stdlib Lock acquire/release (compiled): {ns_per(t3):.1f} ns") + + t4 = best_of(native.bench_stdlib_with, REPEAT) + print(f" stdlib Lock with (compiled): {ns_per(t4):.1f} ns") + + t5 = best_of(bench_interp_acquire_release, REPEAT) + print(f" stdlib Lock acquire/release (interp): {ns_per(t5):.1f} ns") + + t6 = best_of(bench_interp_with, REPEAT) + print(f" stdlib Lock with (interp): {ns_per(t6):.1f} ns") + + print() + print(f" acquire/release: librt vs stdlib compiled: {t3/t1:.2f}x") + print(f" acquire/release: librt vs interp: {t5/t1:.2f}x") + print(f" with: librt vs stdlib compiled: {t4/t2:.2f}x") + print(f" with: librt vs interp: {t6/t2:.2f}x") + +run() From ef358563c9a9ae794ac6adda1cdf8e25ade6dcc5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 11:25:03 +0100 Subject: [PATCH 24/45] Drop unncessary _Atomic in pthread back end --- mypyc/lib-rt/threading/librt_threading.c | 35 ++++++++++++++++-------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 1e2ba5a1d28bb..98d028a4f684b 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -42,7 +42,6 @@ #else #define LOCK_BACKEND_PTHREAD #include -#include #endif #endif @@ -131,7 +130,10 @@ typedef struct { // thread, so pthread's same-thread-unlock rule is never violated. pthread_mutex_t mut; pthread_cond_t lock_released; - _Atomic int locked; // 0=unlocked, 1=locked; protected by `mut` + // 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 @@ -153,7 +155,7 @@ Lock_init_internal(LockObject *self) #else pthread_mutex_init(&self->mut, NULL); pthread_cond_init(&self->lock_released, NULL); - atomic_store_explicit(&self->locked, 0, memory_order_relaxed); + self->locked = 0; #endif } @@ -259,8 +261,8 @@ Lock_acquire_impl(LockObject *self, int blocking) // section. This is what lets a different thread call release(). if (!blocking) { pthread_mutex_lock(&self->mut); - if (!atomic_load_explicit(&self->locked, memory_order_relaxed)) { - atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + if (!self->locked) { + self->locked = 1; pthread_mutex_unlock(&self->mut); return 1; } @@ -270,8 +272,8 @@ Lock_acquire_impl(LockObject *self, int blocking) // Fast path: grab the lock without releasing the GIL if it is free. pthread_mutex_lock(&self->mut); - if (!atomic_load_explicit(&self->locked, memory_order_relaxed)) { - atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + if (!self->locked) { + self->locked = 1; pthread_mutex_unlock(&self->mut); return 1; } @@ -281,10 +283,10 @@ Lock_acquire_impl(LockObject *self, int blocking) // other Python threads can run (and release the lock). Py_BEGIN_ALLOW_THREADS pthread_mutex_lock(&self->mut); - while (atomic_load_explicit(&self->locked, memory_order_relaxed)) { + while (self->locked) { pthread_cond_wait(&self->lock_released, &self->mut); } - atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + self->locked = 1; pthread_mutex_unlock(&self->mut); Py_END_ALLOW_THREADS return 1; @@ -329,11 +331,11 @@ Lock_release_impl(LockObject *self) #else // pthread mutex + condvar fallback pthread_mutex_lock(&self->mut); - if (!atomic_load_explicit(&self->locked, memory_order_relaxed)) { + if (!self->locked) { pthread_mutex_unlock(&self->mut); return -1; } - atomic_store_explicit(&self->locked, 0, memory_order_relaxed); + 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); @@ -355,8 +357,17 @@ Lock_is_locked(LockObject *self) int result = self->locked; ReleaseSRWLockShared(&self->srw); return result; -#else +#elif defined(LOCK_BACKEND_SEM) + // The semaphore backend touches `locked` locklessly (no mutex), so the + // flag is genuinely atomic and read without locking here. return atomic_load_explicit(&self->locked, memory_order_relaxed) != 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 } From d66b7cfda60a92901b79763f1749c63d71afb6a1 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 11:45:26 +0100 Subject: [PATCH 25/45] Avoid atomic ops when using sem_t back end (breaks 3.13t!) --- mypyc/lib-rt/threading/librt_threading.c | 35 +++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 98d028a4f684b..70078ac433058 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -38,7 +38,6 @@ #define LOCK_BACKEND_SEM #include #include -#include #else #define LOCK_BACKEND_PTHREAD #include @@ -115,7 +114,13 @@ typedef struct { // 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. - _Atomic int locked; + // + // Like CPython's semaphore lock, 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). CPython keeps the same flag as a plain `char` in its + // _thread lock wrapper (Modules/_threadmodule.c) for exactly this purpose. + int locked; } LockObject; #else // pthread mutex + condvar fallback @@ -151,7 +156,7 @@ Lock_init_internal(LockObject *self) self->locked = 0; #elif defined(LOCK_BACKEND_SEM) sem_init(&self->sem, 0, 1); - atomic_store_explicit(&self->locked, 0, memory_order_relaxed); + self->locked = 0; #else pthread_mutex_init(&self->mut, NULL); pthread_cond_init(&self->lock_released, NULL); @@ -223,7 +228,7 @@ Lock_acquire_impl(LockObject *self, int blocking) status = sem_trywait(&self->sem); } while (status == -1 && errno == EINTR); if (status == 0) { - atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + self->locked = 1; return 1; } return 0; // EAGAIN: already held @@ -237,7 +242,7 @@ Lock_acquire_impl(LockObject *self, int blocking) status = sem_trywait(&self->sem); } while (status == -1 && errno == EINTR); if (status == 0) { - atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + self->locked = 1; return 1; } } @@ -252,7 +257,7 @@ Lock_acquire_impl(LockObject *self, int blocking) } while (status == -1 && errno == EINTR); } Py_END_ALLOW_THREADS - atomic_store_explicit(&self->locked, 1, memory_order_relaxed); + self->locked = 1; return 1; #else // pthread mutex + condvar fallback @@ -320,12 +325,16 @@ Lock_release_impl(LockObject *self) return 0; #elif defined(LOCK_BACKEND_SEM) - // Atomically clear the flag; only the caller that observes the previous - // value as 1 actually owns the release and posts the semaphore. This - // prevents a double release from over-incrementing the semaphore. - if (!atomic_exchange_explicit(&self->locked, 0, memory_order_relaxed)) { + // Check-then-clear the flag, then post. This mirrors CPython's _thread + // lock release (a plain `if (!self->locked) error; self->locked = 0` + // guarded by the GIL): the flag is only touched with the GIL held, so the + // check and clear are serialized without an atomic. As in CPython, calling + // release() concurrently from multiple threads on the same lock is a usage + // error and is not defended against here. + if (!self->locked) { return -1; } + self->locked = 0; sem_post(&self->sem); return 0; @@ -358,9 +367,9 @@ Lock_is_locked(LockObject *self) ReleaseSRWLockShared(&self->srw); return result; #elif defined(LOCK_BACKEND_SEM) - // The semaphore backend touches `locked` locklessly (no mutex), so the - // flag is genuinely atomic and read without locking here. - return atomic_load_explicit(&self->locked, memory_order_relaxed) != 0; + // The flag is GIL-serialized (see the struct comment); locked() is a + // plain read, matching CPython's _thread lock. + 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. From 0bd5fee7a138b9eda5451b26c4f1d39bcecd798e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 12:58:14 +0100 Subject: [PATCH 26/45] Use PyMutex on 3.13 as well --- mypyc/lib-rt/threading/librt_threading.c | 63 +++++++++++++++++------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 70078ac433058..a2efd790c31ad 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -5,25 +5,48 @@ #include "librt_threading.h" #include "mypyc_util.h" -#if CPY_3_14_FEATURES +#if CPY_3_13_FEATURES -// Python 3.14+: Use PyMutex (1-byte atomic lock with parking lot). +// Python 3.13+: Use PyMutex (1-byte atomic lock with parking lot). This is +// the same primitive CPython's own threading.Lock uses on 3.13+, and it is +// correct in both the default (GIL) and free-threaded builds. +// // 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" +// PyMutex_LockFast's signature changed in 3.14: it takes a PyMutex* on 3.14+ +// but the raw bits pointer (uint8_t*) on 3.13. Normalize via a shim. +// +// For the blocking slow path we want a lock that detaches the GIL while +// parked. On 3.14 the internal _PyMutex_LockTimed(..., _PY_LOCK_DETACH) is +// exported and used directly. On 3.13 that symbol is declared in +// pycore_lock.h but NOT exported from libpython, so we call the public +// PyMutex_Lock() instead -- which is defined as exactly +// _PyMutex_LockTimed(m, -1, _PY_LOCK_DETACH). Note cpython/lock.h #defines +// PyMutex_Lock to an inline fast-path wrapper; calling it after a failed +// CPY_LOCK_FAST just retries one CAS before parking, which is negligible. +#if CPY_3_14_FEATURES +#define CPY_LOCK_FAST(m) PyMutex_LockFast(&(m)) +#define CPY_LOCK_BLOCKING(m) _PyMutex_LockTimed(&(m), -1, _PY_LOCK_DETACH) +#else +#define CPY_LOCK_FAST(m) PyMutex_LockFast(&(m)._bits) +#define CPY_LOCK_BLOCKING(m) PyMutex_Lock(&(m)) +#endif + #elif defined(_WIN32) -// Python <3.14 on Windows: Use Slim Reader/Writer Lock +// Python <3.13 on Windows: Use Slim Reader/Writer Lock #define LOCK_BACKEND_SRWLOCK #include #else -// Python <3.14 on POSIX. +// Python <3.13 on POSIX. // // Prefer a POSIX unnamed semaphore when the platform supports it well, and // fall back to a pthread mutex + condition variable otherwise. We use the @@ -50,21 +73,23 @@ // // A fast mutex lock for use from mypyc-compiled code. // -// On Python 3.14+, this uses CPython's PyMutex, a 1-byte atomic lock +// On Python 3.13+, 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. +// releases the GIL when blocking. This is the same primitive CPython's own +// threading.Lock uses on 3.13+, and it is correct in both the default (GIL) +// and free-threaded builds. // -// On older Python with Windows, this uses an SRWLOCK (Slim Reader/Writer -// Lock) plus a CONDITION_VARIABLE guarding a `locked` flag. The SRWLOCK only +// On Python 3.12 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 older Python 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): +// On Python 3.12 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), this uses a // sem_t initialized to 1: acquire is sem_wait, release is sem_post. @@ -82,7 +107,7 @@ // ---------- Platform-specific lock state ---------- -#if CPY_3_14_FEATURES +#if defined(LOCK_BACKEND_PYMUTEX) typedef struct { PyObject_HEAD @@ -148,7 +173,7 @@ typedef struct { static inline void Lock_init_internal(LockObject *self) { -#if CPY_3_14_FEATURES +#if defined(LOCK_BACKEND_PYMUTEX) self->mutex = (PyMutex){0}; #elif defined(LOCK_BACKEND_SRWLOCK) InitializeSRWLock(&self->srw); @@ -169,14 +194,14 @@ Lock_init_internal(LockObject *self) static int Lock_acquire_impl(LockObject *self, int blocking) { -#if CPY_3_14_FEATURES +#if defined(LOCK_BACKEND_PYMUTEX) if (!blocking) { - return PyMutex_LockFast(&self->mutex); + return CPY_LOCK_FAST(self->mutex); } - if (PyMutex_LockFast(&self->mutex)) { + if (CPY_LOCK_FAST(self->mutex)) { return 1; } - _PyMutex_LockTimed(&self->mutex, -1, _PY_LOCK_DETACH); + CPY_LOCK_BLOCKING(self->mutex); return 1; #elif defined(LOCK_BACKEND_SRWLOCK) @@ -302,7 +327,7 @@ Lock_acquire_impl(LockObject *self, int blocking) static int Lock_release_impl(LockObject *self) { -#if CPY_3_14_FEATURES +#if defined(LOCK_BACKEND_PYMUTEX) // Note: check-then-unlock is not atomic, but this matches CPython's // threading.Lock semantics. Only the owning thread should call release(). if (!PyMutex_IsLocked(&self->mutex)) { @@ -356,7 +381,7 @@ Lock_release_impl(LockObject *self) static inline int Lock_is_locked(LockObject *self) { -#if CPY_3_14_FEATURES +#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 From 713c5e72c8148f845fdd449468a4482b5a117324 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 13:16:52 +0100 Subject: [PATCH 27/45] Fix race condition --- mypyc/lib-rt/threading/librt_threading.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index a2efd790c31ad..e3ee4c2eb9f94 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -112,6 +112,10 @@ typedef struct { PyObject_HEAD PyMutex mutex; + // Serializes the check-before-unlock path. Public PyMutex_Unlock() aborts + // if the mutex is unlocked, and PyMutex_IsLocked()+PyMutex_Unlock() is not + // atomic in free-threaded builds. + PyMutex release_guard; } LockObject; #elif defined(LOCK_BACKEND_SRWLOCK) @@ -175,6 +179,7 @@ Lock_init_internal(LockObject *self) { #if defined(LOCK_BACKEND_PYMUTEX) self->mutex = (PyMutex){0}; + self->release_guard = (PyMutex){0}; #elif defined(LOCK_BACKEND_SRWLOCK) InitializeSRWLock(&self->srw); InitializeConditionVariable(&self->lock_released); @@ -328,12 +333,18 @@ static int Lock_release_impl(LockObject *self) { #if defined(LOCK_BACKEND_PYMUTEX) - // Note: check-then-unlock is not atomic, but this matches CPython's - // threading.Lock semantics. Only the owning thread should call release(). + // 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, so use a small guard mutex to keep the public + // PyMutex_Unlock() from seeing an already-unlocked mutex under concurrent + // release() calls in free-threaded builds. + PyMutex_Lock(&self->release_guard); if (!PyMutex_IsLocked(&self->mutex)) { + PyMutex_Unlock(&self->release_guard); return -1; } PyMutex_Unlock(&self->mutex); + PyMutex_Unlock(&self->release_guard); return 0; #elif defined(LOCK_BACKEND_SRWLOCK) From 8976171c24d15b60e0d2740dc29f1948527b22bb Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 13:32:45 +0100 Subject: [PATCH 28/45] No longer support 3.13, and with non-blocking lock on 3.14 --- mypyc/lib-rt/threading/librt_threading.c | 47 ++++++------------------ 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index e3ee4c2eb9f94..9a7eccce62601 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -5,12 +5,9 @@ #include "librt_threading.h" #include "mypyc_util.h" -#if CPY_3_13_FEATURES +#if CPY_3_14_FEATURES -// Python 3.13+: Use PyMutex (1-byte atomic lock with parking lot). This is -// the same primitive CPython's own threading.Lock uses on 3.13+, and it is -// correct in both the default (GIL) and free-threaded builds. -// +// Python 3.14+: Use PyMutex (1-byte atomic lock with parking lot). // PyMutex_LockFast, _PyMutex_LockTimed, and _PY_LOCK_DETACH are internal // CPython APIs that might change across minor releases. #define LOCK_BACKEND_PYMUTEX @@ -19,34 +16,15 @@ #endif #include "internal/pycore_lock.h" -// PyMutex_LockFast's signature changed in 3.14: it takes a PyMutex* on 3.14+ -// but the raw bits pointer (uint8_t*) on 3.13. Normalize via a shim. -// -// For the blocking slow path we want a lock that detaches the GIL while -// parked. On 3.14 the internal _PyMutex_LockTimed(..., _PY_LOCK_DETACH) is -// exported and used directly. On 3.13 that symbol is declared in -// pycore_lock.h but NOT exported from libpython, so we call the public -// PyMutex_Lock() instead -- which is defined as exactly -// _PyMutex_LockTimed(m, -1, _PY_LOCK_DETACH). Note cpython/lock.h #defines -// PyMutex_Lock to an inline fast-path wrapper; calling it after a failed -// CPY_LOCK_FAST just retries one CAS before parking, which is negligible. -#if CPY_3_14_FEATURES -#define CPY_LOCK_FAST(m) PyMutex_LockFast(&(m)) -#define CPY_LOCK_BLOCKING(m) _PyMutex_LockTimed(&(m), -1, _PY_LOCK_DETACH) -#else -#define CPY_LOCK_FAST(m) PyMutex_LockFast(&(m)._bits) -#define CPY_LOCK_BLOCKING(m) PyMutex_Lock(&(m)) -#endif - #elif defined(_WIN32) -// Python <3.13 on Windows: Use Slim Reader/Writer Lock +// Python <3.14 on Windows: Use Slim Reader/Writer Lock #define LOCK_BACKEND_SRWLOCK #include #else -// Python <3.13 on POSIX. +// Python <3.14 on POSIX. // // Prefer a POSIX unnamed semaphore when the platform supports it well, and // fall back to a pthread mutex + condition variable otherwise. We use the @@ -73,13 +51,11 @@ // // A fast mutex lock for use from mypyc-compiled code. // -// On Python 3.13+, this uses CPython's PyMutex, a 1-byte atomic lock +// On Python 3.14+, 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. This is the same primitive CPython's own -// threading.Lock uses on 3.13+, and it is correct in both the default (GIL) -// and free-threaded builds. +// releases the GIL when blocking. // -// On Python 3.12 and earlier with Windows, this uses an SRWLOCK (Slim +// 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 @@ -87,7 +63,7 @@ // Python/thread_nt.h) and is the Windows twin of the POSIX pthread+condvar // backend below. // -// On Python 3.12 and earlier with POSIX systems, there are two backends, both +// 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): // @@ -201,12 +177,13 @@ Lock_acquire_impl(LockObject *self, int blocking) { #if defined(LOCK_BACKEND_PYMUTEX) if (!blocking) { - return CPY_LOCK_FAST(self->mutex); + PyLockStatus r = _PyMutex_LockTimed(&self->mutex, 0, _Py_LOCK_DONT_DETACH); + return r == PY_LOCK_ACQUIRED; } - if (CPY_LOCK_FAST(self->mutex)) { + if (PyMutex_LockFast(&self->mutex)) { return 1; } - CPY_LOCK_BLOCKING(self->mutex); + _PyMutex_LockTimed(&self->mutex, -1, _PY_LOCK_DETACH); return 1; #elif defined(LOCK_BACKEND_SRWLOCK) From f0c34e6146f85a1a59620ba571fe88994164375b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 13:38:06 +0100 Subject: [PATCH 29/45] Handle signals on 3.14 --- mypyc/lib-rt/threading/librt_threading.c | 23 +++++++++++++++++++---- mypyc/primitives/librt_threading_ops.py | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 9a7eccce62601..3aef18cd3b38c 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -171,7 +171,8 @@ Lock_init_internal(LockObject *self) } // Try to acquire the lock. Returns 1 (true) on success, 0 (false) if -// non-blocking and the lock is held. +// 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) { @@ -183,7 +184,11 @@ Lock_acquire_impl(LockObject *self, int blocking) if (PyMutex_LockFast(&self->mutex)) { return 1; } - _PyMutex_LockTimed(&self->mutex, -1, _PY_LOCK_DETACH); + 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) @@ -471,6 +476,9 @@ Lock_acquire(LockObject *self, PyObject *const *args, Py_ssize_t nargs, } int result = Lock_acquire_impl(self, blocking); + if (result < 0) { + return NULL; + } return PyBool_FromLong(result); } @@ -549,18 +557,25 @@ Lock_new_internal(void) { } // Acquire the lock (blocking), for use from compiled code. -// Returns true on success. +// 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. +// 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; } diff --git a/mypyc/primitives/librt_threading_ops.py b/mypyc/primitives/librt_threading_ops.py index 4b692e696c167..0bf8183d14355 100644 --- a/mypyc/primitives/librt_threading_ops.py +++ b/mypyc/primitives/librt_threading_ops.py @@ -20,7 +20,7 @@ arg_types=[lock_rprimitive], return_type=bool_rprimitive, c_function_name="LibRTThreading_Lock_acquire_internal", - error_kind=ERR_NEVER, + error_kind=ERR_MAGIC, experimental=True, dependencies=[LIBRT_THREADING], ) @@ -31,7 +31,7 @@ arg_types=[lock_rprimitive, bool_rprimitive], return_type=bool_rprimitive, c_function_name="LibRTThreading_Lock_acquire_blocking_internal", - error_kind=ERR_NEVER, + error_kind=ERR_MAGIC, experimental=True, dependencies=[LIBRT_THREADING], ) From 77cb9a43de0ba26df6f0e1cf09b06c461aa5f390 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 13:42:32 +0100 Subject: [PATCH 30/45] Fix 3.13t builds by using posix implementation --- mypyc/lib-rt/threading/librt_threading.c | 57 ++++++++++++------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 3aef18cd3b38c..3279c45cf9f15 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -26,15 +26,18 @@ // Python <3.14 on POSIX. // -// Prefer a POSIX unnamed semaphore when the platform supports it well, 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. +// 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. #include -#if defined(_POSIX_SEMAPHORES) && (_POSIX_SEMAPHORES + 0) != -1 && \ +#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 @@ -67,16 +70,16 @@ // 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), 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. +// - 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), 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. +// - 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. // #ifdef MYPYC_EXPERIMENTAL @@ -120,11 +123,12 @@ typedef struct { // flag is advisory bookkeeping. It is set after a successful acquire and // cleared before sem_post in release. // - // Like CPython's semaphore lock, 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). CPython keeps the same flag as a plain `char` in its - // _thread lock wrapper (Modules/_threadmodule.c) for exactly this purpose. + // 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). CPython keeps the same flag + // as a plain `char` in its _thread lock wrapper (Modules/_threadmodule.c) + // for exactly this purpose. int locked; } LockObject; @@ -343,12 +347,9 @@ Lock_release_impl(LockObject *self) return 0; #elif defined(LOCK_BACKEND_SEM) - // Check-then-clear the flag, then post. This mirrors CPython's _thread - // lock release (a plain `if (!self->locked) error; self->locked = 0` - // guarded by the GIL): the flag is only touched with the GIL held, so the - // check and clear are serialized without an atomic. As in CPython, calling - // release() concurrently from multiple threads on the same lock is a usage - // error and is not defended against here. + // 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; } From 8022967c37510a485bb87714a32a0de19716d204 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 13:50:33 +0100 Subject: [PATCH 31/45] Make lock acquire interruptible --- mypyc/lib-rt/threading/librt_threading.c | 34 +++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 3279c45cf9f15..3bf0aea35e436 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -264,17 +264,33 @@ Lock_acquire_impl(LockObject *self, int blocking) } // Slow path: block with the GIL dropped so other Python threads can run - // (and release the lock). Retry on EINTR (signal). - Py_BEGIN_ALLOW_THREADS - { + // (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; - do { - status = sem_wait(&self->sem); - } while (status == -1 && errno == EINTR); + 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; + } } - Py_END_ALLOW_THREADS - self->locked = 1; - return 1; #else // pthread mutex + condvar fallback // `mut` only guards `locked` and the condition variable; it is held just From 2902b29e7cf6e4f2595fc5888912868708b0efd5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 13:59:05 +0100 Subject: [PATCH 32/45] Fix error detection --- mypyc/lib-rt/threading/librt_threading.c | 37 +++++++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 3bf0aea35e436..3d8d493fd587d 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -36,12 +36,12 @@ // mutex+condvar fallback because the semaphore backend's `locked` bookkeeping // relies on GIL serialization. #include +#include #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 -#include #else #define LOCK_BACKEND_PTHREAD #include @@ -154,7 +154,7 @@ typedef struct { // ---------- Platform-specific init/acquire/release ---------- -static inline void +static inline int Lock_init_internal(LockObject *self) { #if defined(LOCK_BACKEND_PYMUTEX) @@ -165,13 +165,30 @@ Lock_init_internal(LockObject *self) InitializeConditionVariable(&self->lock_released); self->locked = 0; #elif defined(LOCK_BACKEND_SEM) - sem_init(&self->sem, 0, 1); + if (sem_init(&self->sem, 0, 1) != 0) { + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } self->locked = 0; #else - pthread_mutex_init(&self->mut, NULL); - pthread_cond_init(&self->lock_released, NULL); + 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 @@ -428,6 +445,10 @@ Lock_new(PyTypeObject *type, PyObject *args, PyObject *kwds) } 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; } @@ -444,7 +465,6 @@ Lock_init(LockObject *self, PyObject *args, PyObject *kwds) return -1; } - Lock_init_internal(self); return 0; } @@ -567,8 +587,9 @@ Lock_type_internal(void) { static PyObject * Lock_new_internal(void) { LockObject *self = (LockObject *)LockType.tp_alloc(&LockType, 0); - if (self != NULL) { - Lock_init_internal(self); + if (self != NULL && Lock_init_internal(self) < 0) { + LockType.tp_free((PyObject *)self); + return NULL; } return (PyObject *)self; } From ba0a692d5d525caed24800e9451c96af9b14f603 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 14:24:21 +0100 Subject: [PATCH 33/45] Fix signal behavior when using pthread backend --- mypyc/lib-rt/threading/librt_threading.c | 52 ++++++++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 3d8d493fd587d..72fb999194aa9 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -334,16 +334,50 @@ Lock_acquire_impl(LockObject *self, int blocking) pthread_mutex_unlock(&self->mut); // Slow path: wait for the lock to be released, with the GIL dropped so - // other Python threads can run (and release the lock). - Py_BEGIN_ALLOW_THREADS - pthread_mutex_lock(&self->mut); - while (self->locked) { - pthread_cond_wait(&self->lock_released, &self->mut); + // 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; + } } - self->locked = 1; - pthread_mutex_unlock(&self->mut); - Py_END_ALLOW_THREADS - return 1; #endif } From c0d92113abc038ec110df6fe7e29e536dcb815be Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 15:14:52 +0100 Subject: [PATCH 34/45] Add tests --- mypyc/test-data/run-threading.test | 72 +++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index 3c9b362d97d62..f7e5cdba04279 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -1,8 +1,14 @@ # Test cases for librt.threading (compile and run) [case testLockBasics_librt_experimental] +from typing import Any +from testutil import assertRaises from librt.threading import Lock +class BadBool: + def __bool__(self) -> bool: + raise RuntimeError("bad bool") + def test_lock_basic() -> None: lock = Lock() assert not lock.locked() @@ -13,7 +19,8 @@ def test_lock_basic() -> None: def test_lock_context_manager() -> None: lock = Lock() - with lock: + with lock as acquired: + assert acquired is True assert lock.locked() assert not lock.locked() @@ -87,23 +94,66 @@ def test_acquire_blocking_true() -> None: assert lock.locked() lock.release() +def test_lock_constructor_errors() -> None: + lock_type: Any = Lock + make_type: Any = type + with assertRaises(TypeError): + lock_type(1) + with assertRaises(TypeError): + lock_type(foo=1) + with assertRaises(TypeError): + make_type("LockSubclass", (lock_type,), {}) + +def test_lock_acquire_argument_errors() -> None: + lock: Any = Lock() + with assertRaises(TypeError): + lock.acquire(True, False) + with assertRaises(TypeError): + lock.acquire(foo=True) + +def test_lock_acquire_blocking_truthiness() -> None: + lock: Any = Lock() + assert lock.acquire(blocking=True) + assert lock.locked() + assert not lock.acquire(blocking=False) + lock.release() + + assert lock.acquire(None) + assert lock.locked() + assert not lock.acquire(None) + lock.release() + + assert lock.acquire(1) + lock.release() + assert lock.acquire(0) + lock.release() + +def test_lock_acquire_blocking_bool_error() -> None: + lock: Any = Lock() + with assertRaises(RuntimeError, "bad bool"): + lock.acquire(BadBool()) + with assertRaises(RuntimeError, "bad bool"): + lock.acquire(blocking=BadBool()) + +def test_lock_exit_manual_call() -> None: + lock: Any = Lock() + lock.acquire() + assert lock.__exit__(None, None, None) is None + assert not lock.locked() + + lock.acquire() + assert lock.__exit__() is None + assert not lock.locked() + def test_release_unlocked() -> None: lock = Lock() - try: + with assertRaises(RuntimeError): lock.release() - except RuntimeError: - pass - else: - assert False, "expected RuntimeError" # Also after acquire + release lock.acquire() lock.release() - try: + with assertRaises(RuntimeError): lock.release() - except RuntimeError: - pass - else: - assert False, "expected RuntimeError" [case testLockPerf_librt_experimental_benchmark] import threading From 1279f44aa259383c7766ea7f429ed378494197ba Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 15:33:34 +0100 Subject: [PATCH 35/45] Update comment --- mypyc/lib-rt/threading/librt_threading.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 72fb999194aa9..27685ede0feb5 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -388,9 +388,10 @@ 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, so use a small guard mutex to keep the public - // PyMutex_Unlock() from seeing an already-unlocked mutex under concurrent - // release() calls in free-threaded builds. + // of the public API and is not exported by all CPython builds, so use a + // small guard mutex to keep the public PyMutex_Unlock() from seeing an + // already-unlocked mutex under concurrent release() calls in free-threaded + // builds. PyMutex_Lock(&self->release_guard); if (!PyMutex_IsLocked(&self->mutex)) { PyMutex_Unlock(&self->release_guard); From 7a00ff17b6ca574b8a1e4b500e1b3ced3df3d3b0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 15:41:32 +0100 Subject: [PATCH 36/45] Use sem_t backend on 3.14 + Linux + GIL, as it's faster --- mypyc/lib-rt/threading/librt_threading.c | 31 ++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 27685ede0feb5..219c42940c21a 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -5,9 +5,25 @@ #include "librt_threading.h" #include "mypyc_util.h" -#if CPY_3_14_FEATURES +#if !defined(_WIN32) +#include +#include +#endif + +#if CPY_3_14_FEATURES && defined(__linux__) && !defined(Py_GIL_DISABLED) && \ + defined(_POSIX_SEMAPHORES) && (_POSIX_SEMAPHORES + 0) != -1 && \ + (defined(HAVE_SEM_TIMEDWAIT) || defined(HAVE_SEM_CLOCKWAIT)) -// Python 3.14+: Use PyMutex (1-byte atomic lock with parking lot). +// Python 3.14+ on Linux with the GIL: Use a POSIX unnamed semaphore. On Linux +// this is faster than PyMutex, and the GIL serializes the bookkeeping flag. +#define LOCK_BACKEND_SEM +#include + +#elif CPY_3_14_FEATURES + +// Python 3.14+ outside Linux GIL builds: Use PyMutex (1-byte atomic lock with +// parking lot). PyMutex gives better lock semantics on free-threaded builds, +// macOS, and Windows. // PyMutex_LockFast, _PyMutex_LockTimed, and _PY_LOCK_DETACH are internal // CPython APIs that might change across minor releases. #define LOCK_BACKEND_PYMUTEX @@ -35,8 +51,6 @@ // 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. -#include -#include #if !defined(Py_GIL_DISABLED) && \ defined(_POSIX_SEMAPHORES) && (_POSIX_SEMAPHORES + 0) != -1 && \ (defined(HAVE_SEM_TIMEDWAIT) || defined(HAVE_SEM_CLOCKWAIT)) @@ -54,9 +68,12 @@ // // A fast mutex lock for use from mypyc-compiled code. // -// On Python 3.14+, 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.14+ Linux builds with the GIL, 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. On other Python +// 3.14+ builds, 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 From eb92e210025562c6d9bbf6546bee707ba49cfc26 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 15:46:02 +0100 Subject: [PATCH 37/45] WIP EXPERIMENT --- mypyc/lib-rt/threading/librt_threading.c | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 219c42940c21a..292dd828ae1ef 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -19,11 +19,18 @@ #define LOCK_BACKEND_SEM #include +#elif CPY_3_14_FEATURES && defined(__linux__) && defined(Py_GIL_DISABLED) + +// Python 3.14+ on Linux without the GIL: Experiment with the pthread +// mutex+condvar backend. It is free-threaded-safe because the `locked` +// bookkeeping is protected by the pthread mutex, unlike the sem_t backend. +#define LOCK_BACKEND_PTHREAD +#include + #elif CPY_3_14_FEATURES -// Python 3.14+ outside Linux GIL builds: Use PyMutex (1-byte atomic lock with -// parking lot). PyMutex gives better lock semantics on free-threaded builds, -// macOS, and Windows. +// Python 3.14+ outside Linux builds: Use PyMutex (1-byte atomic lock with +// parking lot). PyMutex gives better lock semantics on macOS and Windows. // PyMutex_LockFast, _PyMutex_LockTimed, and _PY_LOCK_DETACH are internal // CPython APIs that might change across minor releases. #define LOCK_BACKEND_PYMUTEX @@ -70,10 +77,11 @@ // // On Python 3.14+ Linux builds with the GIL, 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. On other Python -// 3.14+ builds, 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. +// concept, so cross-thread release is directly well-defined. On Python 3.14+ +// Linux free-threaded builds, this experimentally uses the pthread mutex + +// condition variable backend described below. On other Python 3.14+ builds, +// 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 From 4ae93d4524ce3158f148174b6d22ec41fad964e8 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 3 Jun 2026 16:31:56 +0100 Subject: [PATCH 38/45] Faster PyMutex implementation (semantic deviation from CPython) --- mypyc/lib-rt/threading/librt_threading.c | 39 ++++++++---------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 292dd828ae1ef..12b86c6bf5421 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -19,18 +19,11 @@ #define LOCK_BACKEND_SEM #include -#elif CPY_3_14_FEATURES && defined(__linux__) && defined(Py_GIL_DISABLED) - -// Python 3.14+ on Linux without the GIL: Experiment with the pthread -// mutex+condvar backend. It is free-threaded-safe because the `locked` -// bookkeeping is protected by the pthread mutex, unlike the sem_t backend. -#define LOCK_BACKEND_PTHREAD -#include - #elif CPY_3_14_FEATURES -// Python 3.14+ outside Linux builds: Use PyMutex (1-byte atomic lock with -// parking lot). PyMutex gives better lock semantics on macOS and Windows. +// Python 3.14+ Linux free-threaded and non-Linux builds: Use PyMutex (1-byte +// atomic lock with parking lot). PyMutex gives better interruptibility than the +// pthread fallback. // PyMutex_LockFast, _PyMutex_LockTimed, and _PY_LOCK_DETACH are internal // CPython APIs that might change across minor releases. #define LOCK_BACKEND_PYMUTEX @@ -77,11 +70,10 @@ // // On Python 3.14+ Linux builds with the GIL, 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. On Python 3.14+ -// Linux free-threaded builds, this experimentally uses the pthread mutex + -// condition variable backend described below. On other Python 3.14+ builds, -// 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. +// concept, so cross-thread release is directly well-defined. On other Python +// 3.14+ builds, 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 @@ -116,10 +108,6 @@ typedef struct { PyObject_HEAD PyMutex mutex; - // Serializes the check-before-unlock path. Public PyMutex_Unlock() aborts - // if the mutex is unlocked, and PyMutex_IsLocked()+PyMutex_Unlock() is not - // atomic in free-threaded builds. - PyMutex release_guard; } LockObject; #elif defined(LOCK_BACKEND_SRWLOCK) @@ -184,7 +172,6 @@ Lock_init_internal(LockObject *self) { #if defined(LOCK_BACKEND_PYMUTEX) self->mutex = (PyMutex){0}; - self->release_guard = (PyMutex){0}; #elif defined(LOCK_BACKEND_SRWLOCK) InitializeSRWLock(&self->srw); InitializeConditionVariable(&self->lock_released); @@ -413,17 +400,15 @@ 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, so use a - // small guard mutex to keep the public PyMutex_Unlock() from seeing an - // already-unlocked mutex under concurrent release() calls in free-threaded - // builds. - PyMutex_Lock(&self->release_guard); + // 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)) { - PyMutex_Unlock(&self->release_guard); return -1; } PyMutex_Unlock(&self->mutex); - PyMutex_Unlock(&self->release_guard); return 0; #elif defined(LOCK_BACKEND_SRWLOCK) From 042d7701035cbe8bba9e85f457cba2bd9c5c87d0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 17 Jun 2026 16:14:03 +0100 Subject: [PATCH 39/45] Use PyMutex always on 3.14+ --- mypyc/lib-rt/threading/librt_threading.c | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index 12b86c6bf5421..efb520b2a1bb3 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -10,20 +10,11 @@ #include #endif -#if CPY_3_14_FEATURES && defined(__linux__) && !defined(Py_GIL_DISABLED) && \ - defined(_POSIX_SEMAPHORES) && (_POSIX_SEMAPHORES + 0) != -1 && \ - (defined(HAVE_SEM_TIMEDWAIT) || defined(HAVE_SEM_CLOCKWAIT)) - -// Python 3.14+ on Linux with the GIL: Use a POSIX unnamed semaphore. On Linux -// this is faster than PyMutex, and the GIL serializes the bookkeeping flag. -#define LOCK_BACKEND_SEM -#include - -#elif CPY_3_14_FEATURES +#if CPY_3_14_FEATURES -// Python 3.14+ Linux free-threaded and non-Linux builds: Use PyMutex (1-byte +// 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. +// 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 @@ -68,12 +59,9 @@ // // A fast mutex lock for use from mypyc-compiled code. // -// On Python 3.14+ Linux builds with the GIL, 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. On other Python -// 3.14+ builds, 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.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 From 8ba5e62e3d2b38280e7676fec9177ce4e29ea9d4 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 7 Jul 2026 14:24:51 +0100 Subject: [PATCH 40/45] Remove benchmark --- mypyc/test-data/run-threading.test | 110 ----------------------------- 1 file changed, 110 deletions(-) diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index f7e5cdba04279..7f8f34c75adc5 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -154,113 +154,3 @@ def test_release_unlocked() -> None: lock.release() with assertRaises(RuntimeError): lock.release() - -[case testLockPerf_librt_experimental_benchmark] -import threading -from librt.threading import Lock - -N = 5000000 - -def bench_librt_acquire_release() -> None: - lock = Lock() - n = N - i = 0 - while i < n: - lock.acquire() - lock.release() - i += 1 - -def bench_librt_with() -> None: - lock = Lock() - n = N - i = 0 - while i < n: - with lock: - pass - i += 1 - -def bench_stdlib_acquire_release() -> None: - lock = threading.Lock() - n = N - i = 0 - while i < n: - lock.acquire() - lock.release() - i += 1 - -def bench_stdlib_with() -> None: - lock = threading.Lock() - n = N - i = 0 - while i < n: - with lock: - pass - i += 1 - -[file driver.py] -import time -import threading -import native - -N = native.N -REPEAT = 5 - -def bench_interp_acquire_release() -> None: - lock = threading.Lock() - n = N - i = 0 - while i < n: - lock.acquire() - lock.release() - i += 1 - -def bench_interp_with() -> None: - lock = threading.Lock() - n = N - i = 0 - while i < n: - with lock: - pass - i += 1 - -def best_of(func: object, repeat: int) -> float: - best = float("inf") - for r in range(repeat): - t0 = time.perf_counter() - func() # type: ignore[operator] - dt = time.perf_counter() - t0 - best = min(best, dt) - return best - -def ns_per(seconds: float) -> float: - return seconds / N * 1e9 - -def run() -> None: - print(f"Lock acquire/release and with, {N} iterations, best of {REPEAT}") - print() - - t1 = best_of(native.bench_librt_acquire_release, REPEAT) - print(f" librt Lock acquire/release (compiled): {ns_per(t1):.1f} ns") - - t2 = best_of(native.bench_librt_with, REPEAT) - print(f" librt Lock with (compiled): {ns_per(t2):.1f} ns") - - t3 = best_of(native.bench_stdlib_acquire_release, REPEAT) - print(f" stdlib Lock acquire/release (compiled): {ns_per(t3):.1f} ns") - - t4 = best_of(native.bench_stdlib_with, REPEAT) - print(f" stdlib Lock with (compiled): {ns_per(t4):.1f} ns") - - t5 = best_of(bench_interp_acquire_release, REPEAT) - print(f" stdlib Lock acquire/release (interp): {ns_per(t5):.1f} ns") - - t6 = best_of(bench_interp_with, REPEAT) - print(f" stdlib Lock with (interp): {ns_per(t6):.1f} ns") - - print() - print(f" acquire/release: librt vs stdlib compiled: {t3/t1:.2f}x") - print(f" acquire/release: librt vs interp: {t5/t1:.2f}x") - print(f" with: librt vs stdlib compiled: {t4/t2:.2f}x") - print(f" with: librt vs interp: {t6/t2:.2f}x") - -run() From f6901bb42e735f5912871216157f037554d6c78b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 7 Jul 2026 14:47:19 +0100 Subject: [PATCH 41/45] Remove the experimental gating --- mypyc/ir/rtypes.py | 2 +- mypyc/irbuild/statement.py | 2 +- mypyc/lib-rt/threading/librt_threading.c | 10 -------- mypyc/lib-rt/threading/librt_threading_api.c | 8 ------ mypyc/lib-rt/threading/librt_threading_api.h | 4 --- mypyc/primitives/librt_threading_ops.py | 5 ---- mypyc/test-data/irbuild-threading.test | 27 +++----------------- mypyc/test-data/run-threading.test | 2 +- 8 files changed, 7 insertions(+), 53 deletions(-) diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index c28a09e85547f..1a5515d5621a8 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -556,7 +556,7 @@ def __hash__(self) -> int: is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_THREADING,), - ) + ), } bytes_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.BytesWriter"] diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index cb2155283b057..e90aa089305f2 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -116,6 +116,7 @@ restore_exc_info_op, ) from mypyc.primitives.generic_ops import iter_op, next_raw_op, py_delattr_op +from mypyc.primitives.librt_threading_ops import lock_acquire_op, lock_release_op from mypyc.primitives.misc_ops import ( check_stop_op, coro_op, @@ -128,7 +129,6 @@ type_op, yield_from_except_op, ) -from mypyc.primitives.librt_threading_ops import lock_acquire_op, lock_release_op from .match import MatchVisitor diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index efb520b2a1bb3..bc5ebf04deabd 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -87,8 +87,6 @@ // unlocked on the same thread that locked it. // -#ifdef MYPYC_EXPERIMENTAL - // ---------- Platform-specific lock state ---------- #if defined(LOCK_BACKEND_PYMUTEX) @@ -667,14 +665,10 @@ Lock_locked_internal(PyObject *self) { return (char)Lock_is_locked((LockObject *)self); } -#endif // MYPYC_EXPERIMENTAL - static PyMethodDef librt_threading_module_methods[] = { {NULL, NULL, 0, NULL} }; -#ifdef MYPYC_EXPERIMENTAL - static int threading_abi_version(void) { return LIBRT_THREADING_ABI_VERSION; @@ -685,12 +679,9 @@ threading_api_version(void) { return LIBRT_THREADING_API_VERSION; } -#endif - static int librt_threading_module_exec(PyObject *m) { -#ifdef MYPYC_EXPERIMENTAL if (PyType_Ready(&LockType) < 0) { return -1; } @@ -713,7 +704,6 @@ librt_threading_module_exec(PyObject *m) if (PyModule_Add(m, "_C_API", c_api_object) < 0) { return -1; } -#endif // MYPYC_EXPERIMENTAL return 0; } diff --git a/mypyc/lib-rt/threading/librt_threading_api.c b/mypyc/lib-rt/threading/librt_threading_api.c index a93af80b9d773..156975d243638 100644 --- a/mypyc/lib-rt/threading/librt_threading_api.c +++ b/mypyc/lib-rt/threading/librt_threading_api.c @@ -1,17 +1,10 @@ #include "librt_threading_api.h" -#ifdef MYPYC_EXPERIMENTAL - void *LibRTThreading_API[LIBRT_THREADING_API_LEN] = {0}; -#endif - int import_librt_threading(void) { -#ifndef MYPYC_EXPERIMENTAL - return 0; -#else PyObject *mod = PyImport_ImportModule("librt.threading"); if (mod == NULL) return -1; @@ -47,5 +40,4 @@ import_librt_threading(void) // entries, so this copy is safe. memcpy(LibRTThreading_API, capsule, sizeof(LibRTThreading_API)); return 0; -#endif } diff --git a/mypyc/lib-rt/threading/librt_threading_api.h b/mypyc/lib-rt/threading/librt_threading_api.h index 4df15b889785b..acb3414d79561 100644 --- a/mypyc/lib-rt/threading/librt_threading_api.h +++ b/mypyc/lib-rt/threading/librt_threading_api.h @@ -6,8 +6,6 @@ int import_librt_threading(void); -#ifdef MYPYC_EXPERIMENTAL - extern void *LibRTThreading_API[LIBRT_THREADING_API_LEN]; #define LibRTThreading_ABIVersion (*(int (*)(void)) LibRTThreading_API[0]) @@ -19,6 +17,4 @@ extern void *LibRTThreading_API[LIBRT_THREADING_API_LEN]; #define LibRTThreading_Lock_locked_internal (*(char (*)(PyObject *self)) LibRTThreading_API[6]) #define LibRTThreading_Lock_acquire_blocking_internal (*(char (*)(PyObject *self, char blocking)) LibRTThreading_API[7]) -#endif // MYPYC_EXPERIMENTAL - #endif // LIBRT_THREADING_API_H diff --git a/mypyc/primitives/librt_threading_ops.py b/mypyc/primitives/librt_threading_ops.py index 0bf8183d14355..03c003ca0cf4f 100644 --- a/mypyc/primitives/librt_threading_ops.py +++ b/mypyc/primitives/librt_threading_ops.py @@ -10,7 +10,6 @@ return_type=lock_rprimitive, c_function_name="LibRTThreading_Lock_new_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_THREADING], ) @@ -21,7 +20,6 @@ return_type=bool_rprimitive, c_function_name="LibRTThreading_Lock_acquire_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_THREADING], ) @@ -32,7 +30,6 @@ return_type=bool_rprimitive, c_function_name="LibRTThreading_Lock_acquire_blocking_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_THREADING], ) @@ -43,7 +40,6 @@ return_type=none_rprimitive, c_function_name="LibRTThreading_Lock_release_internal", error_kind=ERR_MAGIC, - experimental=True, dependencies=[LIBRT_THREADING], ) @@ -54,6 +50,5 @@ return_type=bool_rprimitive, c_function_name="LibRTThreading_Lock_locked_internal", error_kind=ERR_NEVER, - experimental=True, dependencies=[LIBRT_THREADING], ) diff --git a/mypyc/test-data/irbuild-threading.test b/mypyc/test-data/irbuild-threading.test index 964c259d0906d..652ccedcb9d75 100644 --- a/mypyc/test-data/irbuild-threading.test +++ b/mypyc/test-data/irbuild-threading.test @@ -1,4 +1,4 @@ -[case testLockBasics_experimental] +[case testLockBasics] from librt.threading import Lock def lock_create() -> Lock: @@ -37,7 +37,7 @@ L0: r0 = LibRTThreading_Lock_locked_internal(lk) return r0 -[case testLockAcquireRelease_experimental] +[case testLockAcquireRelease] from librt.threading import Lock def acquire_release() -> None: @@ -56,7 +56,7 @@ L0: r2 = LibRTThreading_Lock_release_internal(lk) return 1 -[case testLockAcquireBlocking_experimental] +[case testLockAcquireBlocking] from librt.threading import Lock def lock_acquire_blocking(lk: Lock, b: bool) -> bool: @@ -69,7 +69,7 @@ L0: r0 = LibRTThreading_Lock_acquire_blocking_internal(lk, b) return r0 -[case testLockWith_experimental] +[case testLockWith] from librt.threading import Lock def with_lock(lk: Lock) -> None: @@ -113,22 +113,3 @@ L10: unreachable L11: return 1 - -[case testLockExperimentalDisabled] -from librt.threading import Lock - -def create_lock() -> Lock: - return Lock() -[out] -def create_lock(): - r0 :: dict - r1 :: str - r2, r3 :: object - r4 :: librt.threading.Lock -L0: - r0 = __main__.globals :: static - r1 = 'Lock' - r2 = CPyDict_GetItem(r0, r1) - r3 = PyObject_Vectorcall(r2, 0, 0, 0) - r4 = cast(librt.threading.Lock, r3) - return r4 diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index 7f8f34c75adc5..721aa677a8135 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -1,6 +1,6 @@ # Test cases for librt.threading (compile and run) -[case testLockBasics_librt_experimental] +[case testLockBasics_librt] from typing import Any from testutil import assertRaises from librt.threading import Lock From 556b06fe787b09fc044339c4f2f7eb611163c616 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 7 Jul 2026 15:24:46 +0100 Subject: [PATCH 42/45] Update some comments --- mypyc/lib-rt/threading/librt_threading.c | 10 ++++++---- mypyc/primitives/librt_threading_ops.py | 2 +- mypyc/test-data/run-threading.test | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index bc5ebf04deabd..fa711c6173d56 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -125,9 +125,10 @@ typedef struct { // 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). CPython keeps the same flag - // as a plain `char` in its _thread lock wrapper (Modules/_threadmodule.c) - // for exactly this purpose. + // 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; @@ -451,7 +452,8 @@ Lock_is_locked(LockObject *self) return result; #elif defined(LOCK_BACKEND_SEM) // The flag is GIL-serialized (see the struct comment); locked() is a - // plain read, matching CPython's _thread lock. + // 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. diff --git a/mypyc/primitives/librt_threading_ops.py b/mypyc/primitives/librt_threading_ops.py index 03c003ca0cf4f..c060742dc2050 100644 --- a/mypyc/primitives/librt_threading_ops.py +++ b/mypyc/primitives/librt_threading_ops.py @@ -13,7 +13,7 @@ dependencies=[LIBRT_THREADING], ) -# Lock.acquire() -- blocking acquire, always returns True +# Lock.acquire() -- blocking acquire, returns True unless it raises lock_acquire_op = method_op( name="acquire", arg_types=[lock_rprimitive], diff --git a/mypyc/test-data/run-threading.test b/mypyc/test-data/run-threading.test index 721aa677a8135..8af8755b76c44 100644 --- a/mypyc/test-data/run-threading.test +++ b/mypyc/test-data/run-threading.test @@ -54,7 +54,7 @@ def test_contention() -> None: def test_cross_thread_release() -> None: # threading.Lock is unowned: a lock acquired on one thread may be - # released from another. A thread blocked in acquire() must then wake up. + # released from another, after which another thread can acquire it. import threading lock = Lock() assert lock.acquire() @@ -71,7 +71,7 @@ def test_cross_thread_release() -> None: t = threading.Thread(target=releaser) t.start() started.wait() - # Blocks until releaser releases the lock on the other thread. + # This may block until releaser releases the lock on the other thread. assert lock.acquire() t.join() assert released.is_set() From da927680a09fdde52c34f43018e588aba140c73c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 7 Jul 2026 17:34:43 +0100 Subject: [PATCH 43/45] Update mypyc/lib-rt/threading/librt_threading.c Co-authored-by: Piotr Sawicki --- mypyc/lib-rt/threading/librt_threading.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index fa711c6173d56..a14fca03a3102 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -556,7 +556,7 @@ static PyObject * Lock_release(LockObject *self, PyObject *Py_UNUSED(ignored)) { if (Lock_release_impl(self) < 0) { - PyErr_SetString(PyExc_RuntimeError, "release unlocked lock"); + PyErr_SetString(PyExc_RuntimeError, "cannot release an unlocked lock"); return NULL; } Py_RETURN_NONE; From 717c79d7ed5b270f32f9e3966e3520980102917b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 7 Jul 2026 17:34:50 +0100 Subject: [PATCH 44/45] Update mypyc/lib-rt/threading/librt_threading.c Co-authored-by: Piotr Sawicki --- mypyc/lib-rt/threading/librt_threading.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index a14fca03a3102..bb071e44dce7f 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -655,7 +655,7 @@ Lock_acquire_blocking_internal(PyObject *self, char blocking) { static char Lock_release_internal(PyObject *self) { if (Lock_release_impl((LockObject *)self) < 0) { - PyErr_SetString(PyExc_RuntimeError, "release unlocked lock"); + PyErr_SetString(PyExc_RuntimeError, "cannot release an unlocked lock"); return 2; } return 0; From badfb1189dc0b098a2689cb54bd0f72c94d2a6c4 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 7 Jul 2026 17:47:30 +0100 Subject: [PATCH 45/45] Address feedback --- mypyc/lib-rt/threading/librt_threading.c | 57 ++++++++---------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/mypyc/lib-rt/threading/librt_threading.c b/mypyc/lib-rt/threading/librt_threading.c index bb071e44dce7f..84d7346a75872 100644 --- a/mypyc/lib-rt/threading/librt_threading.c +++ b/mypyc/lib-rt/threading/librt_threading.c @@ -215,18 +215,9 @@ Lock_acquire_impl(LockObject *self, int blocking) // `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(). - if (!blocking) { - AcquireSRWLockExclusive(&self->srw); - if (!self->locked) { - self->locked = 1; - ReleaseSRWLockExclusive(&self->srw); - return 1; - } - ReleaseSRWLockExclusive(&self->srw); - return 0; - } - - // Fast path: grab the lock without releasing the GIL if it is free. + // + // 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; @@ -234,6 +225,9 @@ Lock_acquire_impl(LockObject *self, int blocking) 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). @@ -254,20 +248,10 @@ Lock_acquire_impl(LockObject *self, int blocking) // 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. - if (!blocking) { - int status; - do { - status = sem_trywait(&self->sem); - } while (status == -1 && errno == EINTR); - if (status == 0) { - self->locked = 1; - return 1; - } - return 0; // EAGAIN: already held - } - - // Fast path: try non-blocking acquire first to avoid GIL release/reacquire - // overhead in the common uncontended case. + // + // 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 { @@ -278,6 +262,9 @@ Lock_acquire_impl(LockObject *self, int blocking) 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 @@ -312,18 +299,9 @@ Lock_acquire_impl(LockObject *self, int blocking) // `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(). - if (!blocking) { - pthread_mutex_lock(&self->mut); - if (!self->locked) { - self->locked = 1; - pthread_mutex_unlock(&self->mut); - return 1; - } - pthread_mutex_unlock(&self->mut); - return 0; - } - - // Fast path: grab the lock without releasing the GIL if it is free. + // + // 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; @@ -331,6 +309,9 @@ Lock_acquire_impl(LockObject *self, int blocking) 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