Skip to content

Commit 097b783

Browse files
gh-104341: Add a Separate "Running" Lock for Each Thread (gh-104754)
Having a separate lock means Thread.join() doesn't need to wait for the thread to be cleaned up first. It can wait for the thread's Python target to finish running. This gives us some flexibility in how we clean up threads. (This is a minor cleanup as part of a fix for gh-104341.)
1 parent 08b4eb8 commit 097b783

File tree

2 files changed

+53
-41
lines changed

2 files changed

+53
-41
lines changed

Lib/test/test_threading.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ def f():
747747
rc, out, err = assert_python_ok("-c", code)
748748
self.assertEqual(err, b"")
749749

750-
def test_tstate_lock(self):
750+
def test_running_lock(self):
751751
# Test an implementation detail of Thread objects.
752752
started = _thread.allocate_lock()
753753
finish = _thread.allocate_lock()
@@ -757,29 +757,29 @@ def f():
757757
started.release()
758758
finish.acquire()
759759
time.sleep(0.01)
760-
# The tstate lock is None until the thread is started
760+
# The running lock is None until the thread is started
761761
t = threading.Thread(target=f)
762-
self.assertIs(t._tstate_lock, None)
762+
self.assertIs(t._running_lock, None)
763763
t.start()
764764
started.acquire()
765765
self.assertTrue(t.is_alive())
766-
# The tstate lock can't be acquired when the thread is running
766+
# The running lock can't be acquired when the thread is running
767767
# (or suspended).
768-
tstate_lock = t._tstate_lock
769-
self.assertFalse(tstate_lock.acquire(timeout=0), False)
768+
running_lock = t._running_lock
769+
self.assertFalse(running_lock.acquire(timeout=0), False)
770770
finish.release()
771771
# When the thread ends, the state_lock can be successfully
772772
# acquired.
773-
self.assertTrue(tstate_lock.acquire(timeout=support.SHORT_TIMEOUT), False)
774-
# But is_alive() is still True: we hold _tstate_lock now, which
775-
# prevents is_alive() from knowing the thread's end-of-life C code
773+
self.assertTrue(running_lock.acquire(timeout=support.SHORT_TIMEOUT), False)
774+
# But is_alive() is still True: we hold _running_lock now, which
775+
# prevents is_alive() from knowing the thread's Python code
776776
# is done.
777777
self.assertTrue(t.is_alive())
778778
# Let is_alive() find out the C code is done.
779-
tstate_lock.release()
779+
running_lock.release()
780780
self.assertFalse(t.is_alive())
781-
# And verify the thread disposed of _tstate_lock.
782-
self.assertIsNone(t._tstate_lock)
781+
# And verify the thread disposed of _running_lock.
782+
self.assertIsNone(t._running_lock)
783783
t.join()
784784

785785
def test_repr_stopped(self):

Lib/threading.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,7 @@ class is implemented.
908908
self._ident = None
909909
if _HAVE_THREAD_NATIVE_ID:
910910
self._native_id = None
911+
self._running_lock = None
911912
self._tstate_lock = None
912913
self._started = Event()
913914
self._is_stopped = False
@@ -926,13 +927,17 @@ def _reset_internal_locks(self, is_alive):
926927
# bpo-42350: If the fork happens when the thread is already stopped
927928
# (ex: after threading._shutdown() has been called), _tstate_lock
928929
# is None. Do nothing in this case.
930+
if self._running_lock is not None:
931+
self._running_lock._at_fork_reinit()
932+
self._running_lock.acquire()
929933
if self._tstate_lock is not None:
930934
self._tstate_lock._at_fork_reinit()
931935
self._tstate_lock.acquire()
932936
else:
933937
# The thread isn't alive after fork: it doesn't have a tstate
934938
# anymore.
935939
self._is_stopped = True
940+
self._running_lock = None
936941
self._tstate_lock = None
937942

938943
def __repr__(self):
@@ -1019,6 +1024,14 @@ def _set_ident(self):
10191024
def _set_native_id(self):
10201025
self._native_id = get_native_id()
10211026

1027+
def _set_running_lock(self):
1028+
"""
1029+
Set a lock object which will be released by the interpreter when
1030+
the target func has finished running.
1031+
"""
1032+
self._running_lock = _allocate_lock()
1033+
self._running_lock.acquire()
1034+
10221035
def _set_tstate_lock(self):
10231036
"""
10241037
Set a lock object which will be released by the interpreter when
@@ -1035,6 +1048,7 @@ def _set_tstate_lock(self):
10351048
def _bootstrap_inner(self):
10361049
try:
10371050
self._set_ident()
1051+
self._set_running_lock()
10381052
self._set_tstate_lock()
10391053
if _HAVE_THREAD_NATIVE_ID:
10401054
self._set_native_id()
@@ -1054,29 +1068,29 @@ def _bootstrap_inner(self):
10541068
self._invoke_excepthook(self)
10551069
finally:
10561070
self._delete()
1071+
self._running_lock.release()
10571072

10581073
def _stop(self):
10591074
# After calling ._stop(), .is_alive() returns False and .join() returns
1060-
# immediately. ._tstate_lock must be released before calling ._stop().
1075+
# immediately. ._running_lock must be released before calling ._stop().
10611076
#
1062-
# Normal case: C code at the end of the thread's life
1063-
# (release_sentinel in _threadmodule.c) releases ._tstate_lock, and
1064-
# that's detected by our ._wait_for_tstate_lock(), called by .join()
1077+
# Normal case: ._bootstrap_inner() releases ._running_lock, and
1078+
# that's detected by our ._wait_for_running_lock(), called by .join()
10651079
# and .is_alive(). Any number of threads _may_ call ._stop()
10661080
# simultaneously (for example, if multiple threads are blocked in
10671081
# .join() calls), and they're not serialized. That's harmless -
10681082
# they'll just make redundant rebindings of ._is_stopped and
1069-
# ._tstate_lock. Obscure: we rebind ._tstate_lock last so that the
1070-
# "assert self._is_stopped" in ._wait_for_tstate_lock() always works
1071-
# (the assert is executed only if ._tstate_lock is None).
1083+
# ._running_lock. Obscure: we rebind ._running_lock last so that the
1084+
# "assert self._is_stopped" in ._wait_for_running_lock() always works
1085+
# (the assert is executed only if ._running_lock is None).
10721086
#
1073-
# Special case: _main_thread releases ._tstate_lock via this
1087+
# Special case: _main_thread releases ._running_lock via this
10741088
# module's _shutdown() function.
1075-
lock = self._tstate_lock
1089+
lock = self._running_lock
10761090
if lock is not None:
10771091
assert not lock.locked()
10781092
self._is_stopped = True
1079-
self._tstate_lock = None
1093+
self._running_lock = None
10801094
if not self.daemon:
10811095
with _shutdown_locks_lock:
10821096
# Remove our lock and other released locks from _shutdown_locks
@@ -1123,20 +1137,17 @@ def join(self, timeout=None):
11231137
raise RuntimeError("cannot join current thread")
11241138

11251139
if timeout is None:
1126-
self._wait_for_tstate_lock()
1140+
self._wait_for_running_lock()
11271141
else:
11281142
# the behavior of a negative timeout isn't documented, but
11291143
# historically .join(timeout=x) for x<0 has acted as if timeout=0
1130-
self._wait_for_tstate_lock(timeout=max(timeout, 0))
1131-
1132-
def _wait_for_tstate_lock(self, block=True, timeout=-1):
1133-
# Issue #18808: wait for the thread state to be gone.
1134-
# At the end of the thread's life, after all knowledge of the thread
1135-
# is removed from C data structures, C code releases our _tstate_lock.
1136-
# This method passes its arguments to _tstate_lock.acquire().
1137-
# If the lock is acquired, the C code is done, and self._stop() is
1138-
# called. That sets ._is_stopped to True, and ._tstate_lock to None.
1139-
lock = self._tstate_lock
1144+
self._wait_for_running_lock(timeout=max(timeout, 0))
1145+
1146+
def _wait_for_running_lock(self, block=True, timeout=-1):
1147+
# This method passes its arguments to _running_lock.acquire().
1148+
# If the lock is acquired, the python code is done, and self._stop() is
1149+
# called. That sets ._is_stopped to True, and ._running_lock to None.
1150+
lock = self._running_lock
11401151
if lock is None:
11411152
# already determined that the C code is done
11421153
assert self._is_stopped
@@ -1207,7 +1218,7 @@ def is_alive(self):
12071218
assert self._initialized, "Thread.__init__() not called"
12081219
if self._is_stopped or not self._started.is_set():
12091220
return False
1210-
self._wait_for_tstate_lock(False)
1221+
self._wait_for_running_lock(False)
12111222
return not self._is_stopped
12121223

12131224
@property
@@ -1417,7 +1428,7 @@ class _MainThread(Thread):
14171428

14181429
def __init__(self):
14191430
Thread.__init__(self, name="MainThread", daemon=False)
1420-
self._set_tstate_lock()
1431+
self._set_running_lock()
14211432
self._started.set()
14221433
self._set_ident()
14231434
if _HAVE_THREAD_NATIVE_ID:
@@ -1558,7 +1569,7 @@ def _shutdown():
15581569
# dubious, but some code does it. We can't wait for C code to release
15591570
# the main thread's tstate_lock - that won't happen until the interpreter
15601571
# is nearly dead. So we release it here. Note that just calling _stop()
1561-
# isn't enough: other threads may already be waiting on _tstate_lock.
1572+
# isn't enough: other threads may already be waiting on _running_lock.
15621573
if _main_thread._is_stopped:
15631574
# _shutdown() was already called
15641575
return
@@ -1573,12 +1584,13 @@ def _shutdown():
15731584

15741585
# Main thread
15751586
if _main_thread.ident == get_ident():
1576-
tlock = _main_thread._tstate_lock
1577-
# The main thread isn't finished yet, so its thread state lock can't
1587+
assert _main_thread._tstate_lock is None
1588+
running_lock = _main_thread._running_lock
1589+
# The main thread isn't finished yet, so its running lock can't
15781590
# have been released.
1579-
assert tlock is not None
1580-
assert tlock.locked()
1581-
tlock.release()
1591+
assert running_lock is not None
1592+
assert running_lock.locked()
1593+
running_lock.release()
15821594
_main_thread._stop()
15831595
else:
15841596
# bpo-1596321: _shutdown() must be called in the main thread.

0 commit comments

Comments
 (0)