From 6e6e06b7895e0d5d345e71bac56e20e99277922e Mon Sep 17 00:00:00 2001 From: Peter Hawkins Date: Tue, 14 Jul 2026 20:08:50 +0000 Subject: [PATCH] gh-152075: Avoid lock contention in _Py_Specialize_LoadGlobal under free threading Under high thread concurrency in free-threaded builds, `_Py_Specialize_LoadGlobal`suffers from lock contention when acquiring the critical section mutexes for the `globals` and `builtins` dictionaries during bytecode specialization. This PR introduces non-blocking critical section entry (`PyCriticalSection2_TryBegin`) and updates LOAD_GLOBAL bytecode specialization to attempt a non-blocking lock acquisition. If acquiring the two object mutexes would block, specialization is skipped. --- Include/cpython/critical_section.h | 9 ++++ Include/internal/pycore_critical_section.h | 54 +++++++++++++++++++ ...-07-14-20-19-48.gh-issue-152075.V9Rwhp.rst | 2 + Python/critical_section.c | 11 ++++ Python/specialize.c | 13 ++++- 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-14-20-19-48.gh-issue-152075.V9Rwhp.rst diff --git a/Include/cpython/critical_section.h b/Include/cpython/critical_section.h index bcba32da412f326..7b384e1c360dec6 100644 --- a/Include/cpython/critical_section.h +++ b/Include/cpython/critical_section.h @@ -41,6 +41,15 @@ PyCriticalSection_BeginMutex(PyCriticalSection *c, PyMutex *m); PyAPI_FUNC(void) PyCriticalSection2_BeginMutex(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2); +// Attempts to enter a critical section locking both `a` and `b` without +// blocking. Returns non-zero (1) if successful, in which case the critical +// section is active and must be terminated with `PyCriticalSection2_End(c)`. +// Returns 0 if acquiring the locks would block, in which case no locks are +// acquired and the critical section is not entered. On GIL-enabled builds, +// this always succeeds and returns 1. +PyAPI_FUNC(int) +PyCriticalSection2_TryBegin(PyCriticalSection2 *c, PyObject *a, PyObject *b); + #ifndef Py_GIL_DISABLED #undef Py_BEGIN_CRITICAL_SECTION #undef Py_END_CRITICAL_SECTION diff --git a/Include/internal/pycore_critical_section.h b/Include/internal/pycore_critical_section.h index 51d99d74ca159f6..5ba76e27af3b7dc 100644 --- a/Include/internal/pycore_critical_section.h +++ b/Include/internal/pycore_critical_section.h @@ -193,6 +193,60 @@ _PyCriticalSection2_Begin(PyThreadState *tstate, PyCriticalSection2 *c, PyObject _PyCriticalSection2_BeginMutex(tstate, c, &a->ob_mutex, &b->ob_mutex); } +static inline int +_PyCriticalSection_TryBeginMutex(PyThreadState *tstate, PyCriticalSection *c, PyMutex *m) +{ + if (PyMutex_LockFast(m)) { + c->_cs_mutex = m; + c->_cs_prev = tstate->critical_section; + tstate->critical_section = (uintptr_t)c; + return 1; + } + return 0; +} + +static inline int +_PyCriticalSection2_TryBeginMutex(PyThreadState *tstate, PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2) +{ + if (m1 == m2) { + c->_cs_mutex2 = NULL; + return _PyCriticalSection_TryBeginMutex(tstate, &c->_cs_base, m1); + } + + if ((uintptr_t)m2 < (uintptr_t)m1) { + PyMutex *tmp = m1; + m1 = m2; + m2 = tmp; + } + + if (PyMutex_LockFast(m1)) { + if (PyMutex_LockFast(m2)) { + c->_cs_base._cs_mutex = m1; + c->_cs_mutex2 = m2; + c->_cs_base._cs_prev = tstate->critical_section; + + uintptr_t p = (uintptr_t)c | _Py_CRITICAL_SECTION_TWO_MUTEXES; + tstate->critical_section = p; + return 1; + } + else { + PyMutex_Unlock(m1); + return 0; + } + } + return 0; +} + +// Attempts to enter a two-object critical section without blocking. +// Returns 1 if both mutexes were locked immediately, in which case the +// critical section is entered and must be exited with `_PyCriticalSection2_End`. +// Returns 0 if locking would block, leaving no locks held. +static inline int +_PyCriticalSection2_TryBegin(PyThreadState *tstate, PyCriticalSection2 *c, PyObject *a, PyObject *b) +{ + return _PyCriticalSection2_TryBeginMutex(tstate, c, &a->ob_mutex, &b->ob_mutex); +} + static inline void _PyCriticalSection2_End(PyThreadState *tstate, PyCriticalSection2 *c) { diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-14-20-19-48.gh-issue-152075.V9Rwhp.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-14-20-19-48.gh-issue-152075.V9Rwhp.rst new file mode 100644 index 000000000000000..84c7d36b97acba0 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-14-20-19-48.gh-issue-152075.V9Rwhp.rst @@ -0,0 +1,2 @@ +Reduced lock contention during LOAD_GLOBAL bytecode specialization under +free threading. diff --git a/Python/critical_section.c b/Python/critical_section.c index dbee6f236a73bea..2d81425cee292cb 100644 --- a/Python/critical_section.c +++ b/Python/critical_section.c @@ -217,3 +217,14 @@ PyCriticalSection2_End(PyCriticalSection2 *c) _PyCriticalSection2_End(_PyThreadState_GET(), c); #endif } + +#undef PyCriticalSection2_TryBegin +int +PyCriticalSection2_TryBegin(PyCriticalSection2 *c, PyObject *a, PyObject *b) +{ +#ifdef Py_GIL_DISABLED + return _PyCriticalSection2_TryBegin(_PyThreadState_GET(), c, a, b); +#else + return 1; +#endif +} diff --git a/Python/specialize.c b/Python/specialize.c index 773b55c329c7720..e405ded531b0206 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -1457,9 +1457,18 @@ _Py_Specialize_LoadGlobal( PyObject *globals, PyObject *builtins, _Py_CODEUNIT *instr, PyObject *name) { - Py_BEGIN_CRITICAL_SECTION2(globals, builtins); +#ifdef Py_GIL_DISABLED + PyCriticalSection2 cs; + if (PyCriticalSection2_TryBegin(&cs, globals, builtins)) { + specialize_load_global_lock_held(globals, builtins, instr, name); + PyCriticalSection2_End(&cs); + } + else { + unspecialize(instr); + } +#else specialize_load_global_lock_held(globals, builtins, instr, name); - Py_END_CRITICAL_SECTION2(); +#endif } static int