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