]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-115942: Add `locked` to several multiprocessing locks (#115944)
authorsobolevn <mail@sobolevn.me>
Tue, 8 Apr 2025 08:14:12 +0000 (11:14 +0300)
committerGitHub <noreply@github.com>
Tue, 8 Apr 2025 08:14:12 +0000 (11:14 +0300)
Co-authored-by: mpage <mpage@cs.stanford.edu>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Doc/library/multiprocessing.rst
Doc/library/threading.rst
Lib/importlib/_bootstrap.py
Lib/multiprocessing/managers.py
Lib/multiprocessing/synchronize.py
Lib/test/_test_multiprocessing.py
Lib/test/lock_tests.py
Lib/threading.py
Misc/NEWS.d/next/Library/2025-04-01-11-16-22.gh-issue-115942.4W3hNx.rst [new file with mode: 0644]
Modules/_threadmodule.c

index 9f987035553b2fd4a9e85f646794139b532a7de1..96036988d420dcf4d6ad0f94d245b4557a490efb 100644 (file)
@@ -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`.
index 00511df32e4388817f3ac3bdff2f9e6aa5b299a7..d205e17d4d9b1d2ddf49a9831f8041f9d70f464d 100644 (file)
@@ -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
index f5635265fbeebfa083533d8186f486e0e9f27939..499da1e04efea8d3442cc05622d5b066cd311aba 100644 (file)
@@ -382,6 +382,9 @@ class _ModuleLock:
                     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)}'
 
index c1f09d2b40905287405de4636bb1d971f31f4e43..91bcf243e78e5b8b90e377b37f302fd20bb2faa3 100644 (file)
@@ -1059,12 +1059,14 @@ class IteratorProxy(BaseProxy):
 
 
 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 @@ class AcquirerProxy(BaseProxy):
 
 
 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):
index edd6c2543a74352c2baf4c9100877b90e5029266..771f1db8813852dbb30f2f1ca07216696243ccfa 100644 (file)
@@ -90,6 +90,9 @@ class SemLock(object):
         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__()
 
index dcce57629efe5bcc152bc79497b6cc47b0863656..1cd5704905f95c6123d9bfeabe9a40c30bf4024e 100644 (file)
@@ -1486,8 +1486,10 @@ class _TestLock(BaseTestCase):
     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 @@ class _TestLock(BaseTestCase):
     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 @@ class TestSyncManagerTypes(unittest.TestCase):
     @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 @@ class TestSyncManagerTypes(unittest.TestCase):
     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)
 
index 8c8f8901f00178b121c01e8212296f3a039a8b6b..009e04e9c0b5226964e92300657d53136c5954ae 100644 (file)
@@ -353,6 +353,18 @@ class RLockTests(BaseLockTests):
         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()
index da9cdf0b09d83cc1057128e16fb0bbe658bdef10..0dc1d324c98ff28661c54cb13ecddb67ff39d930 100644 (file)
@@ -241,6 +241,10 @@ class _RLock:
     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 @@ class Condition:
         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 (file)
index 0000000..8c3538c
--- /dev/null
@@ -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.
index f4c98ca39f6ee6305a7904c68aa346a8000d3033..9f6ac21c8a8ccf3960266929dd6547161eb11655 100644 (file)
@@ -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,