Skip to content

Commit 9489afc

Browse files
committed
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.
1 parent d807210 commit 9489afc

5 files changed

Lines changed: 87 additions & 2 deletions

File tree

Include/critical_section.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b);
6868
PyAPI_FUNC(void)
6969
PyCriticalSection2_End(PyCriticalSection2 *c);
7070

71+
// Attempts to enter a critical section locking both `a` and `b` without
72+
// blocking. Returns non-zero (1) if successful, in which case the critical
73+
// section is active and must be terminated with `PyCriticalSection2_End(c)`.
74+
// Returns 0 if acquiring the locks would block, in which case no locks are
75+
// acquired and the critical section is not entered. On GIL-enabled builds,
76+
// this always succeeds and returns 1.
77+
PyAPI_FUNC(int)
78+
PyCriticalSection2_TryBegin(PyCriticalSection2 *c, PyObject *a, PyObject *b);
79+
7180
// These are definitions for the stable ABI. For GIL-ful builds they're
7281
// conditionally redefined as no-ops in cpython/critical_section.h.
7382

Include/internal/pycore_critical_section.h

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,60 @@ _PyCriticalSection2_Begin(PyThreadState *tstate, PyCriticalSection2 *c, PyObject
193193
_PyCriticalSection2_BeginMutex(tstate, c, &a->ob_mutex, &b->ob_mutex);
194194
}
195195

196+
static inline int
197+
_PyCriticalSection_TryBeginMutex(PyThreadState *tstate, PyCriticalSection *c, PyMutex *m)
198+
{
199+
if (PyMutex_LockFast(m)) {
200+
c->_cs_mutex = m;
201+
c->_cs_prev = tstate->critical_section;
202+
tstate->critical_section = (uintptr_t)c;
203+
return 1;
204+
}
205+
return 0;
206+
}
207+
208+
static inline int
209+
_PyCriticalSection2_TryBeginMutex(PyThreadState *tstate, PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2)
210+
{
211+
if (m1 == m2) {
212+
c->_cs_mutex2 = NULL;
213+
return _PyCriticalSection_TryBeginMutex(tstate, &c->_cs_base, m1);
214+
}
215+
216+
if ((uintptr_t)m2 < (uintptr_t)m1) {
217+
PyMutex *tmp = m1;
218+
m1 = m2;
219+
m2 = tmp;
220+
}
221+
222+
if (PyMutex_LockFast(m1)) {
223+
if (PyMutex_LockFast(m2)) {
224+
c->_cs_base._cs_mutex = m1;
225+
c->_cs_mutex2 = m2;
226+
c->_cs_base._cs_prev = tstate->critical_section;
227+
228+
uintptr_t p = (uintptr_t)c | _Py_CRITICAL_SECTION_TWO_MUTEXES;
229+
tstate->critical_section = p;
230+
return 1;
231+
}
232+
else {
233+
PyMutex_Unlock(m1);
234+
return 0;
235+
}
236+
}
237+
return 0;
238+
}
239+
240+
// Attempts to enter a two-object critical section without blocking.
241+
// Returns 1 if both mutexes were locked immediately, in which case the
242+
// critical section is entered and must be exited with `_PyCriticalSection2_End`.
243+
// Returns 0 if locking would block, leaving no locks held.
244+
static inline int
245+
_PyCriticalSection2_TryBegin(PyThreadState *tstate, PyCriticalSection2 *c, PyObject *a, PyObject *b)
246+
{
247+
return _PyCriticalSection2_TryBeginMutex(tstate, c, &a->ob_mutex, &b->ob_mutex);
248+
}
249+
196250
static inline void
197251
_PyCriticalSection2_End(PyThreadState *tstate, PyCriticalSection2 *c)
198252
{
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Reduced lock contention during LOAD_GLOBAL bytecode specialization under
2+
free threading.

Python/critical_section.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,14 @@ PyCriticalSection2_End(PyCriticalSection2 *c)
217217
_PyCriticalSection2_End(_PyThreadState_GET(), c);
218218
#endif
219219
}
220+
221+
#undef PyCriticalSection2_TryBegin
222+
int
223+
PyCriticalSection2_TryBegin(PyCriticalSection2 *c, PyObject *a, PyObject *b)
224+
{
225+
#ifdef Py_GIL_DISABLED
226+
return _PyCriticalSection2_TryBegin(_PyThreadState_GET(), c, a, b);
227+
#else
228+
return 1;
229+
#endif
230+
}

Python/specialize.c

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,9 +1457,18 @@ _Py_Specialize_LoadGlobal(
14571457
PyObject *globals, PyObject *builtins,
14581458
_Py_CODEUNIT *instr, PyObject *name)
14591459
{
1460-
Py_BEGIN_CRITICAL_SECTION2(globals, builtins);
1460+
#ifdef Py_GIL_DISABLED
1461+
PyCriticalSection2 cs;
1462+
if (PyCriticalSection2_TryBegin(&cs, globals, builtins)) {
1463+
specialize_load_global_lock_held(globals, builtins, instr, name);
1464+
PyCriticalSection2_End(&cs);
1465+
}
1466+
else {
1467+
unspecialize(instr);
1468+
}
1469+
#else
14611470
specialize_load_global_lock_held(globals, builtins, instr, name);
1462-
Py_END_CRITICAL_SECTION2();
1471+
#endif
14631472
}
14641473

14651474
static int

0 commit comments

Comments
 (0)