From dde2ca2798bd7db2d2d581acea9c92dbaf91bf12 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 26 Feb 2024 15:08:45 +0300 Subject: [PATCH 1/9] gh-115942: Add `locked` to several multiprocessing locks --- Lib/multiprocessing/managers.py | 6 ++++-- Lib/multiprocessing/synchronize.py | 3 +++ Lib/test/_test_multiprocessing.py | 13 +++++++++++-- Modules/_threadmodule.c | 13 +++++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Lib/multiprocessing/managers.py b/Lib/multiprocessing/managers.py index 76b915de74d94e..575c80649542e6 100644 --- a/Lib/multiprocessing/managers.py +++ b/Lib/multiprocessing/managers.py @@ -1051,12 +1051,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): @@ -1064,7 +1066,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 3ccbfe311c71f3..4311f0607721d7 100644 --- a/Lib/multiprocessing/synchronize.py +++ b/Lib/multiprocessing/synchronize.py @@ -91,6 +91,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 f70a693e641b4e..49dd810cb0340c 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -1348,23 +1348,32 @@ class _TestLock(BaseTestCase): def test_lock(self): lock = self.Lock() self.assertEqual(lock.acquire(), True) + self.assertEqual(lock.locked(), True) self.assertEqual(lock.acquire(False), False) self.assertEqual(lock.release(), None) + self.assertFalse(lock.locked()) self.assertRaises((ValueError, threading.ThreadError), lock.release) 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): diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 4c2185cc7ea1fd..262b1ebe899078 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -578,6 +578,17 @@ Do note that if the lock was acquire()d several times in a row by the\n\ current thread, release() needs to be called as many times for the lock\n\ to be available for other threads."); +static PyObject * +rlock_locked(rlockobject *self, PyObject *Py_UNUSED(ignored)) +{ + return PyBool_FromLong(self->rlock_count); +} + +PyDoc_STRVAR(rlock_locked_doc, +"locked()\n\ +\n\ +Returns whether this lock is locked right now or not."); + static PyObject * rlock_acquire_restore(rlockobject *self, PyObject *args) { @@ -719,6 +730,8 @@ static PyMethodDef rlock_methods[] = { METH_VARARGS | METH_KEYWORDS, rlock_acquire_doc}, {"release", (PyCFunction)rlock_release, METH_NOARGS, rlock_release_doc}, + {"locked", (PyCFunction)rlock_locked, + METH_NOARGS, rlock_locked_doc}, {"_is_owned", (PyCFunction)rlock_is_owned, METH_NOARGS, rlock_is_owned_doc}, {"_acquire_restore", (PyCFunction)rlock_acquire_restore, From 646c650fcb7234fa7dca34c78bf74263f64ee1b3 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sat, 22 Jun 2024 10:27:19 +0300 Subject: [PATCH 2/9] Update Modules/_threadmodule.c Co-authored-by: mpage --- Modules/_threadmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 262b1ebe899078..866205f68cfdb0 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -581,7 +581,7 @@ to be available for other threads."); static PyObject * rlock_locked(rlockobject *self, PyObject *Py_UNUSED(ignored)) { - return PyBool_FromLong(self->rlock_count); + return PyBool_FromLong(_Py_atomic_load_ullong_relaxed(&self->rlock_owner) != 0); } PyDoc_STRVAR(rlock_locked_doc, From 7f548e1f1fdfed835686ebde8dac4e2e3e484672 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 1 Apr 2025 12:17:17 +0300 Subject: [PATCH 3/9] Ready to be reviewed --- Doc/library/multiprocessing.rst | 14 ++++++++++++++ Lib/test/_test_multiprocessing.py | 2 +- .../2025-04-01-11-16-22.gh-issue-115942.4W3hNx.rst | 5 +++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-04-01-11-16-22.gh-issue-115942.4W3hNx.rst diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 9f987035553b2f..88852e4f020513 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() + + Returns a boolean whether this object is locked right now or not. + + .. 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() + + Returns a boolean whether this object is locked right now or not. + + .. versionadded:: next + + .. class:: Semaphore([value]) A semaphore object: a close analog of :class:`threading.Semaphore`. diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 79072202277810..fecd4e3a514507 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -1486,7 +1486,7 @@ def test_repr_lock(self): def test_lock(self): lock = self.Lock() self.assertEqual(lock.acquire(), True) - self.assertEqual(lock.locked(), True) + self.assertTrue(lock.locked()) self.assertEqual(lock.acquire(False), False) self.assertEqual(lock.release(), None) self.assertFalse(lock.locked()) 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. From 5b21db593c370280cbfbeeeb6405c3870923f425 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 1 Apr 2025 12:26:37 +0300 Subject: [PATCH 4/9] Add more tests --- Lib/test/_test_multiprocessing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index fecd4e3a514507..1cd5704905f95c 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6263,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)() @@ -6274,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) From 6038ee60f38597b0c27abcfd6fbf46e63e525fc7 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 1 Apr 2025 12:38:35 +0300 Subject: [PATCH 5/9] More tests --- Doc/library/threading.rst | 6 ++++++ Lib/test/lock_tests.py | 12 ++++++++++++ Lib/threading.py | 7 ++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 0326631d5b13f2..04d9ee6896104c 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -808,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() + + Returns a boolean whether this object is locked right now or not. + + .. versionadded:: next + .. method:: wait(timeout=None) Wait until notified or until a timeout occurs. If the calling thread has 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..8e60398e4fc691 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): + """Returns whether or not 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(). From e77a8eeba6672a1807277cbaff1962d46694778d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 1 Apr 2025 12:43:22 +0300 Subject: [PATCH 6/9] Address wording review --- Doc/library/multiprocessing.rst | 4 ++-- Doc/library/threading.rst | 4 ++-- Modules/_threadmodule.c | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 88852e4f020513..d2be7baf4c5857 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -1423,7 +1423,7 @@ object -- see :ref:`multiprocessing-managers`. .. method:: locked() - Returns a boolean whether this object is locked right now or not. + Return a boolean indicating whether this object is locked right now or not. .. versionadded:: next @@ -1490,7 +1490,7 @@ object -- see :ref:`multiprocessing-managers`. .. method:: locked() - Returns a boolean whether this object is locked right now or not. + Return a boolean indicating whether this object is locked right now or not. .. versionadded:: next diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 04d9ee6896104c..da0a074426fdbc 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -711,7 +711,7 @@ call release as many times the lock has been acquired can lead to deadlock. .. method:: locked() - Returns a boolean whether this object is locked right now or not. + Return a boolean indicating whether this object is locked right now or not. .. versionadded:: next @@ -810,7 +810,7 @@ item to the buffer only needs to wake up one consumer thread. .. method:: locked() - Returns a boolean whether this object is locked right now or not. + Return a boolean indicating whether this object is locked right now or not. .. versionadded:: next diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 33fdfd84af7ba8..a041306660798f 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1097,7 +1097,7 @@ rlock_locked(PyObject *op, PyObject *Py_UNUSED(ignored)) PyDoc_STRVAR(rlock_locked_doc, "locked()\n\ \n\ -Returns whether this lock is locked right now or not."); +Return a boolean indicating whether this object is locked right now or not."); static PyObject * rlock_acquire_restore(PyObject *op, PyObject *args) From e72111bf5e862d423ba0531d9cfd0c1a1f1b6979 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 1 Apr 2025 12:51:40 +0300 Subject: [PATCH 7/9] Address review --- Lib/threading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/threading.py b/Lib/threading.py index 8e60398e4fc691..254abeab77dd9e 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -242,7 +242,7 @@ def __exit__(self, t, v, tb): self.release() def locked(self): - """Returns whether or not this object is locked.""" + """Return whether or not this object is locked.""" return self._count > 0 # Internal methods used by condition variables From 98ee84b77e625a5042f9864e22980dad429f290c Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 1 Apr 2025 12:56:46 +0300 Subject: [PATCH 8/9] Add `_ModuleLock.locked` --- Lib/importlib/_bootstrap.py | 3 +++ 1 file changed, 3 insertions(+) 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)}' From 1a50a2096b7d78f6fedd47f1c01ff10f97965ef1 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 7 Apr 2025 16:00:09 +0300 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Doc/library/multiprocessing.rst | 4 ++-- Doc/library/threading.rst | 4 ++-- Lib/threading.py | 2 +- Modules/_threadmodule.c | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index d2be7baf4c5857..96036988d420dc 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -1423,7 +1423,7 @@ object -- see :ref:`multiprocessing-managers`. .. method:: locked() - Return a boolean indicating whether this object is locked right now or not. + Return a boolean indicating whether this object is locked right now. .. versionadded:: next @@ -1490,7 +1490,7 @@ object -- see :ref:`multiprocessing-managers`. .. method:: locked() - Return a boolean indicating whether this object is locked right now or not. + Return a boolean indicating whether this object is locked right now. .. versionadded:: next diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index da0a074426fdbc..d205e17d4d9b1d 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -711,7 +711,7 @@ call release as many times the lock has been acquired can lead to deadlock. .. method:: locked() - Return a boolean indicating whether this object is locked right now or not. + Return a boolean indicating whether this object is locked right now. .. versionadded:: next @@ -810,7 +810,7 @@ item to the buffer only needs to wake up one consumer thread. .. method:: locked() - Return a boolean indicating whether this object is locked right now or not. + Return a boolean indicating whether this object is locked right now. .. versionadded:: next diff --git a/Lib/threading.py b/Lib/threading.py index 254abeab77dd9e..0dc1d324c98ff2 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -242,7 +242,7 @@ def __exit__(self, t, v, tb): self.release() def locked(self): - """Return whether or not this object is locked.""" + """Return whether this object is locked.""" return self._count > 0 # Internal methods used by condition variables diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index a041306660798f..9f6ac21c8a8ccf 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1097,7 +1097,7 @@ rlock_locked(PyObject *op, PyObject *Py_UNUSED(ignored)) PyDoc_STRVAR(rlock_locked_doc, "locked()\n\ \n\ -Return a boolean indicating whether this object is locked right now or not."); +Return a boolean indicating whether this object is locked right now."); static PyObject * rlock_acquire_restore(PyObject *op, PyObject *args)