https://github.com/python/cpython/commit/f7305a06c7a322d23b39ad9d16af814d467624c6
commit: f7305a06c7a322d23b39ad9d16af814d467624c6
branch: main
author: sobolevn <m...@sobolevn.me>
committer: sobolevn <m...@sobolevn.me>
date: 2025-04-08T11:14:12+03:00
summary:

gh-115942: Add `locked` to several multiprocessing locks (#115944)

Co-authored-by: mpage <mp...@cs.stanford.edu>
Co-authored-by: Hugo van Kemenade <1324225+hug...@users.noreply.github.com>

files:
A Misc/NEWS.d/next/Library/2025-04-01-11-16-22.gh-issue-115942.4W3hNx.rst
M Doc/library/multiprocessing.rst
M Doc/library/threading.rst
M Lib/importlib/_bootstrap.py
M Lib/multiprocessing/managers.py
M Lib/multiprocessing/synchronize.py
M Lib/test/_test_multiprocessing.py
M Lib/test/lock_tests.py
M Lib/threading.py
M Modules/_threadmodule.c

diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst
index 9f987035553b2f..96036988d420dc 100644
--- a/Doc/library/multiprocessing.rst
+++ b/Doc/library/multiprocessing.rst
@@ -1421,6 +1421,13 @@ object -- see :ref:`multiprocessing-managers`.
       when invoked on an unlocked lock, a :exc:`ValueError` is raised.
 
 
+   .. method:: locked()
+
+      Return a boolean indicating whether this object is locked right now.
+
+      .. versionadded:: next
+
+
 .. class:: RLock()
 
    A recursive lock object: a close analog of :class:`threading.RLock`.  A
@@ -1481,6 +1488,13 @@ object -- see :ref:`multiprocessing-managers`.
       differs from the implemented behavior in :meth:`threading.RLock.release`.
 
 
+   .. method:: locked()
+
+      Return a boolean indicating whether this object is locked right now.
+
+      .. versionadded:: next
+
+
 .. class:: Semaphore([value])
 
    A semaphore object: a close analog of :class:`threading.Semaphore`.
diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst
index 00511df32e4388..d205e17d4d9b1d 100644
--- a/Doc/library/threading.rst
+++ b/Doc/library/threading.rst
@@ -709,6 +709,13 @@ call release as many times the lock has been acquired can 
lead to deadlock.
       There is no return value.
 
 
+   .. method:: locked()
+
+      Return a boolean indicating whether this object is locked right now.
+
+      .. versionadded:: next
+
+
 .. _condition-objects:
 
 Condition Objects
@@ -801,6 +808,12 @@ item to the buffer only needs to wake up one consumer 
thread.
       Release the underlying lock. This method calls the corresponding method 
on
       the underlying lock; there is no return value.
 
+   .. method:: locked()
+
+      Return a boolean indicating whether this object is locked right now.
+
+      .. versionadded:: next
+
    .. method:: wait(timeout=None)
 
       Wait until notified or until a timeout occurs. If the calling thread has
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py
index f5635265fbeebf..499da1e04efea8 100644
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -382,6 +382,9 @@ def release(self):
                     self.waiters.pop()
                     self.wakeup.release()
 
+    def locked(self):
+        return bool(self.count)
+
     def __repr__(self):
         return f'_ModuleLock({self.name!r}) at {id(self)}'
 
diff --git a/Lib/multiprocessing/managers.py b/Lib/multiprocessing/managers.py
index c1f09d2b409052..91bcf243e78e5b 100644
--- a/Lib/multiprocessing/managers.py
+++ b/Lib/multiprocessing/managers.py
@@ -1059,12 +1059,14 @@ def close(self, *args):
 
 
 class AcquirerProxy(BaseProxy):
-    _exposed_ = ('acquire', 'release')
+    _exposed_ = ('acquire', 'release', 'locked')
     def acquire(self, blocking=True, timeout=None):
         args = (blocking,) if timeout is None else (blocking, timeout)
         return self._callmethod('acquire', args)
     def release(self):
         return self._callmethod('release')
+    def locked(self):
+        return self._callmethod('locked')
     def __enter__(self):
         return self._callmethod('acquire')
     def __exit__(self, exc_type, exc_val, exc_tb):
@@ -1072,7 +1074,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
 
 
 class ConditionProxy(AcquirerProxy):
-    _exposed_ = ('acquire', 'release', 'wait', 'notify', 'notify_all')
+    _exposed_ = ('acquire', 'release', 'locked', 'wait', 'notify', 
'notify_all')
     def wait(self, timeout=None):
         return self._callmethod('wait', (timeout,))
     def notify(self, n=1):
diff --git a/Lib/multiprocessing/synchronize.py 
b/Lib/multiprocessing/synchronize.py
index edd6c2543a7435..771f1db8813852 100644
--- a/Lib/multiprocessing/synchronize.py
+++ b/Lib/multiprocessing/synchronize.py
@@ -90,6 +90,9 @@ def _make_methods(self):
         self.acquire = self._semlock.acquire
         self.release = self._semlock.release
 
+    def locked(self):
+        return self._semlock._count() != 0
+
     def __enter__(self):
         return self._semlock.__enter__()
 
diff --git a/Lib/test/_test_multiprocessing.py 
b/Lib/test/_test_multiprocessing.py
index dcce57629efe5b..1cd5704905f95c 100644
--- a/Lib/test/_test_multiprocessing.py
+++ b/Lib/test/_test_multiprocessing.py
@@ -1486,8 +1486,10 @@ def test_repr_lock(self):
     def test_lock(self):
         lock = self.Lock()
         self.assertEqual(lock.acquire(), True)
+        self.assertTrue(lock.locked())
         self.assertEqual(lock.acquire(False), False)
         self.assertEqual(lock.release(), None)
+        self.assertFalse(lock.locked())
         self.assertRaises((ValueError, threading.ThreadError), lock.release)
 
     @staticmethod
@@ -1549,16 +1551,23 @@ def test_repr_rlock(self):
     def test_rlock(self):
         lock = self.RLock()
         self.assertEqual(lock.acquire(), True)
+        self.assertTrue(lock.locked())
         self.assertEqual(lock.acquire(), True)
         self.assertEqual(lock.acquire(), True)
         self.assertEqual(lock.release(), None)
+        self.assertTrue(lock.locked())
         self.assertEqual(lock.release(), None)
         self.assertEqual(lock.release(), None)
+        self.assertFalse(lock.locked())
         self.assertRaises((AssertionError, RuntimeError), lock.release)
 
     def test_lock_context(self):
-        with self.Lock():
-            pass
+        with self.Lock() as locked:
+            self.assertTrue(locked)
+
+    def test_rlock_context(self):
+        with self.RLock() as locked:
+            self.assertTrue(locked)
 
 
 class _TestSemaphore(BaseTestCase):
@@ -6254,6 +6263,7 @@ def test_event(self):
     @classmethod
     def _test_lock(cls, obj):
         obj.acquire()
+        obj.locked()
 
     def test_lock(self, lname="Lock"):
         o = getattr(self.manager, lname)()
@@ -6265,8 +6275,9 @@ def test_lock(self, lname="Lock"):
     def _test_rlock(cls, obj):
         obj.acquire()
         obj.release()
+        obj.locked()
 
-    def test_rlock(self, lname="Lock"):
+    def test_rlock(self, lname="RLock"):
         o = getattr(self.manager, lname)()
         self.run_worker(self._test_rlock, o)
 
diff --git a/Lib/test/lock_tests.py b/Lib/test/lock_tests.py
index 8c8f8901f00178..009e04e9c0b522 100644
--- a/Lib/test/lock_tests.py
+++ b/Lib/test/lock_tests.py
@@ -353,6 +353,18 @@ def test_release_unacquired(self):
         lock.release()
         self.assertRaises(RuntimeError, lock.release)
 
+    def test_locked(self):
+        lock = self.locktype()
+        self.assertFalse(lock.locked())
+        lock.acquire()
+        self.assertTrue(lock.locked())
+        lock.acquire()
+        self.assertTrue(lock.locked())
+        lock.release()
+        self.assertTrue(lock.locked())
+        lock.release()
+        self.assertFalse(lock.locked())
+
     def test_release_save_unacquired(self):
         # Cannot _release_save an unacquired lock
         lock = self.locktype()
diff --git a/Lib/threading.py b/Lib/threading.py
index da9cdf0b09d83c..0dc1d324c98ff2 100644
--- a/Lib/threading.py
+++ b/Lib/threading.py
@@ -241,6 +241,10 @@ def release(self):
     def __exit__(self, t, v, tb):
         self.release()
 
+    def locked(self):
+        """Return whether this object is locked."""
+        return self._count > 0
+
     # Internal methods used by condition variables
 
     def _acquire_restore(self, state):
@@ -286,9 +290,10 @@ def __init__(self, lock=None):
         if lock is None:
             lock = RLock()
         self._lock = lock
-        # Export the lock's acquire() and release() methods
+        # Export the lock's acquire(), release(), and locked() methods
         self.acquire = lock.acquire
         self.release = lock.release
+        self.locked = lock.locked
         # If the lock defines _release_save() and/or _acquire_restore(),
         # these override the default implementations (which just call
         # release() and acquire() on the lock).  Ditto for _is_owned().
diff --git 
a/Misc/NEWS.d/next/Library/2025-04-01-11-16-22.gh-issue-115942.4W3hNx.rst 
b/Misc/NEWS.d/next/Library/2025-04-01-11-16-22.gh-issue-115942.4W3hNx.rst
new file mode 100644
index 00000000000000..8c3538c88d91be
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-04-01-11-16-22.gh-issue-115942.4W3hNx.rst
@@ -0,0 +1,5 @@
+Add :meth:`threading.RLock.locked`,
+:meth:`multiprocessing.Lock.locked`,
+:meth:`multiprocessing.RLock.locked`,
+and allow :meth:`multiprocessing.managers.SyncManager.Lock` and
+:meth:`multiprocessing.managers.SyncManager.RLock` to proxy ``locked()`` call.
diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c
index f4c98ca39f6ee6..9f6ac21c8a8ccf 100644
--- a/Modules/_threadmodule.c
+++ b/Modules/_threadmodule.c
@@ -1086,6 +1086,19 @@ PyDoc_STRVAR(rlock_exit_doc,
 \n\
 Release the lock.");
 
+static PyObject *
+rlock_locked(PyObject *op, PyObject *Py_UNUSED(ignored))
+{
+    rlockobject *self = rlockobject_CAST(op);
+    int is_locked = _PyRecursiveMutex_IsLockedByCurrentThread(&self->lock);
+    return PyBool_FromLong(is_locked);
+}
+
+PyDoc_STRVAR(rlock_locked_doc,
+"locked()\n\
+\n\
+Return a boolean indicating whether this object is locked right now.");
+
 static PyObject *
 rlock_acquire_restore(PyObject *op, PyObject *args)
 {
@@ -1204,6 +1217,8 @@ static PyMethodDef rlock_methods[] = {
      METH_VARARGS | METH_KEYWORDS, rlock_acquire_doc},
     {"release",      rlock_release,
      METH_NOARGS, rlock_release_doc},
+    {"locked",       rlock_locked,
+     METH_NOARGS, rlock_locked_doc},
     {"_is_owned",     rlock_is_owned,
      METH_NOARGS, rlock_is_owned_doc},
     {"_acquire_restore", rlock_acquire_restore,

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to