]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-114203: skip locking if object is already locked by two-mutex critical...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 14 Nov 2025 18:38:30 +0000 (19:38 +0100)
committerGitHub <noreply@github.com>
Fri, 14 Nov 2025 18:38:30 +0000 (18:38 +0000)
gh-114203: skip locking if object is already locked by two-mutex critical section (GH-141476)
(cherry picked from commit f26ed455d5582a7d66618acf2a93bc4b22a84b47)

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-16-25-15.gh-issue-114203.n3tlQO.rst [new file with mode: 0644]
Modules/_testinternalcapi/test_critical_sections.c
Python/critical_section.c

diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-16-25-15.gh-issue-114203.n3tlQO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-16-25-15.gh-issue-114203.n3tlQO.rst
new file mode 100644 (file)
index 0000000..883f933
--- /dev/null
@@ -0,0 +1 @@
+Skip locking if object is already locked by two-mutex critical section.
index e0ba37abcdd33293d879676ebdf51ddfd8413fec..e3b2fe716d48d3178be437c2f8503d51b1599833 100644 (file)
@@ -284,10 +284,111 @@ test_critical_sections_gc(PyObject *self, PyObject *Py_UNUSED(args))
 
 #endif
 
+#ifdef Py_GIL_DISABLED
+
+static PyObject *
+test_critical_section1_reacquisition(PyObject *self, PyObject *Py_UNUSED(args))
+{
+    PyObject *a = PyDict_New();
+    assert(a != NULL);
+
+    PyCriticalSection cs1, cs2;
+    // First acquisition of critical section on object locks it
+    PyCriticalSection_Begin(&cs1, a);
+    assert(PyMutex_IsLocked(&a->ob_mutex));
+    assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+    assert(_PyThreadState_GET()->critical_section == (uintptr_t)&cs1);
+    // Attempting to re-acquire critical section on same object which
+    // is already locked by top-most critical section is a no-op.
+    PyCriticalSection_Begin(&cs2, a);
+    assert(PyMutex_IsLocked(&a->ob_mutex));
+    assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+    assert(_PyThreadState_GET()->critical_section == (uintptr_t)&cs1);
+    // Releasing second critical section is a no-op.
+    PyCriticalSection_End(&cs2);
+    assert(PyMutex_IsLocked(&a->ob_mutex));
+    assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+    assert(_PyThreadState_GET()->critical_section == (uintptr_t)&cs1);
+    // Releasing first critical section unlocks the object
+    PyCriticalSection_End(&cs1);
+    assert(!PyMutex_IsLocked(&a->ob_mutex));
+
+    Py_DECREF(a);
+    Py_RETURN_NONE;
+}
+
+static PyObject *
+test_critical_section2_reacquisition(PyObject *self, PyObject *Py_UNUSED(args))
+{
+    PyObject *a = PyDict_New();
+    assert(a != NULL);
+    PyObject *b = PyDict_New();
+    assert(b != NULL);
+
+    PyCriticalSection2 cs;
+    // First acquisition of critical section on objects locks them
+    PyCriticalSection2_Begin(&cs, a, b);
+    assert(PyMutex_IsLocked(&a->ob_mutex));
+    assert(PyMutex_IsLocked(&b->ob_mutex));
+    assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+    assert((_PyThreadState_GET()->critical_section &
+                  ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+
+    // Attempting to re-acquire critical section on either of two
+    // objects already locked by top-most critical section is a no-op.
+
+    // Check re-acquiring on first object
+    PyCriticalSection a_cs;
+    PyCriticalSection_Begin(&a_cs, a);
+    assert(PyMutex_IsLocked(&a->ob_mutex));
+    assert(PyMutex_IsLocked(&b->ob_mutex));
+    assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+    assert((_PyThreadState_GET()->critical_section &
+                  ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+    // Releasing critical section on either object is a no-op.
+    PyCriticalSection_End(&a_cs);
+    assert(PyMutex_IsLocked(&a->ob_mutex));
+    assert(PyMutex_IsLocked(&b->ob_mutex));
+    assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+    assert((_PyThreadState_GET()->critical_section &
+                  ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+
+    // Check re-acquiring on second object
+    PyCriticalSection b_cs;
+    PyCriticalSection_Begin(&b_cs, b);
+    assert(PyMutex_IsLocked(&a->ob_mutex));
+    assert(PyMutex_IsLocked(&b->ob_mutex));
+    assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+    assert((_PyThreadState_GET()->critical_section &
+                  ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+    // Releasing critical section on either object is a no-op.
+    PyCriticalSection_End(&b_cs);
+    assert(PyMutex_IsLocked(&a->ob_mutex));
+    assert(PyMutex_IsLocked(&b->ob_mutex));
+    assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+    assert((_PyThreadState_GET()->critical_section &
+                  ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+
+    // Releasing critical section on both objects unlocks them
+    PyCriticalSection2_End(&cs);
+    assert(!PyMutex_IsLocked(&a->ob_mutex));
+    assert(!PyMutex_IsLocked(&b->ob_mutex));
+
+    Py_DECREF(a);
+    Py_DECREF(b);
+    Py_RETURN_NONE;
+}
+
+#endif // Py_GIL_DISABLED
+
 static PyMethodDef test_methods[] = {
     {"test_critical_sections", test_critical_sections, METH_NOARGS},
     {"test_critical_sections_nest", test_critical_sections_nest, METH_NOARGS},
     {"test_critical_sections_suspend", test_critical_sections_suspend, METH_NOARGS},
+#ifdef Py_GIL_DISABLED
+    {"test_critical_section1_reacquisition", test_critical_section1_reacquisition, METH_NOARGS},
+    {"test_critical_section2_reacquisition", test_critical_section2_reacquisition, METH_NOARGS},
+#endif
 #ifdef Py_CAN_START_THREADS
     {"test_critical_sections_threads", test_critical_sections_threads, METH_NOARGS},
     {"test_critical_sections_gc", test_critical_sections_gc, METH_NOARGS},
index e628ba2f6d19bcd9f09233bb1efe6e65b5b3652e..218b580e95176d7d5ac8cadb007cb73e929546be 100644 (file)
@@ -24,11 +24,24 @@ _PyCriticalSection_BeginSlow(PyCriticalSection *c, PyMutex *m)
     // As an optimisation for locking the same object recursively, skip
     // locking if the mutex is currently locked by the top-most critical
     // section.
-    if (tstate->critical_section &&
-        untag_critical_section(tstate->critical_section)->_cs_mutex == m) {
-        c->_cs_mutex = NULL;
-        c->_cs_prev = 0;
-        return;
+    // If the top-most critical section is a two-mutex critical section,
+    // then locking is skipped if either mutex is m.
+    if (tstate->critical_section) {
+        PyCriticalSection *prev = untag_critical_section(tstate->critical_section);
+        if (prev->_cs_mutex == m) {
+            c->_cs_mutex = NULL;
+            c->_cs_prev = 0;
+            return;
+        }
+        if (tstate->critical_section & _Py_CRITICAL_SECTION_TWO_MUTEXES) {
+            PyCriticalSection2 *prev2 = (PyCriticalSection2 *)
+                untag_critical_section(tstate->critical_section);
+            if (prev2->_cs_mutex2 == m) {
+                c->_cs_mutex = NULL;
+                c->_cs_prev = 0;
+                return;
+            }
+        }
     }
     c->_cs_mutex = NULL;
     c->_cs_prev = (uintptr_t)tstate->critical_section;