diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 9f987035553..96036988d42 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 00511df32e4..d205e17d4d9 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 f5635265fbe..499da1e04ef 100644 --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -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)}' diff --git a/Lib/multiprocessing/managers.py b/Lib/multiprocessing/managers.py index c1f09d2b409..91bcf243e78 100644 --- a/Lib/multiprocessing/managers.py +++ b/Lib/multiprocessing/managers.py @@ -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): diff --git a/Lib/multiprocessing/synchronize.py b/Lib/multiprocessing/synchronize.py index edd6c2543a7..771f1db8813 100644 --- a/Lib/multiprocessing/synchronize.py +++ b/Lib/multiprocessing/synchronize.py @@ -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__() diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index dcce57629ef..1cd5704905f 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -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) diff --git a/Lib/test/lock_tests.py b/Lib/test/lock_tests.py index 8c8f8901f00..009e04e9c0b 100644 --- a/Lib/test/lock_tests.py +++ b/Lib/test/lock_tests.py @@ -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() diff --git a/Lib/threading.py b/Lib/threading.py index da9cdf0b09d..0dc1d324c98 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -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 index 00000000000..8c3538c88d9 --- /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 f4c98ca39f6..9f6ac21c8a8 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,