]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-135871: Reload lock internal state while spinning in `PyMutex_LockTimed` (gh-146064)
authorDaniele Parmeggiani <8658291+dpdani@users.noreply.github.com>
Mon, 23 Mar 2026 20:55:06 +0000 (21:55 +0100)
committerGitHub <noreply@github.com>
Mon, 23 Mar 2026 20:55:06 +0000 (16:55 -0400)
Add atomic loads in the slow path of PyMutex to increase the number
of lock acquisitions per second that threads can make on a shared mutex.

Misc/NEWS.d/next/Core_and_Builtins/2026-03-19-16-16-40.gh-issue-135871.jSExZ3.rst [new file with mode: 0644]
Python/lock.c

diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-19-16-16-40.gh-issue-135871.jSExZ3.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-19-16-16-40.gh-issue-135871.jSExZ3.rst
new file mode 100644 (file)
index 0000000..29103e4
--- /dev/null
@@ -0,0 +1 @@
+Improve multithreaded scaling of PyMutex in low-contention scenarios by reloading the lock's internal state, without slowing down high-contention scenarios.
index ad97bfd93c8495ae7b27297a274e90253f7e1301..752a5899e088a51d8dc821e449055f822bbabe93 100644 (file)
@@ -27,8 +27,10 @@ static const PyTime_t TIME_TO_BE_FAIR_NS = 1000*1000;
 // enabled.
 #if Py_GIL_DISABLED
 static const int MAX_SPIN_COUNT = 40;
+static const int RELOAD_SPIN_MASK = 3;
 #else
 static const int MAX_SPIN_COUNT = 0;
+static const int RELOAD_SPIN_MASK = 1;
 #endif
 
 struct mutex_entry {
@@ -79,6 +81,16 @@ _PyMutex_LockTimed(PyMutex *m, PyTime_t timeout, _PyLockFlags flags)
     };
 
     Py_ssize_t spin_count = 0;
+#ifdef Py_GIL_DISABLED
+    // Using thread-id as a way of reducing contention further in the reload below.
+    // It adds a pseudo-random starting offset to the recurrence, so that threads
+    // are less likely to try and run compare-exchange at the same time.
+    // The lower bits of platform thread ids are likely to not be random,
+    // hence the right shift.
+    const Py_ssize_t tid = (Py_ssize_t)(_Py_ThreadId() >> 12);
+#else
+    const Py_ssize_t tid = 0;
+#endif
     for (;;) {
         if ((v & _Py_LOCKED) == 0) {
             // The lock is unlocked. Try to grab it.
@@ -92,6 +104,9 @@ _PyMutex_LockTimed(PyMutex *m, PyTime_t timeout, _PyLockFlags flags)
             // Spin for a bit.
             _Py_yield();
             spin_count++;
+            if (((spin_count + tid) & RELOAD_SPIN_MASK) == 0) {
+                v = _Py_atomic_load_uint8_relaxed(&m->_bits);
+            }
             continue;
         }