Skip to content

Commit e40a97a

Browse files
authored
[3.8] bpo-36402: Fix threading._shutdown() race condition (GH-13948) (GH-14050)
* bpo-36402: Fix threading._shutdown() race condition (GH-13948) Fix a race condition at Python shutdown when waiting for threads. Wait until the Python thread state of all non-daemon threads get deleted (join all non-daemon threads), rather than just wait until Python threads complete. * Add threading._shutdown_locks: set of Thread._tstate_lock locks of non-daemon threads used by _shutdown() to wait until all Python thread states get deleted. See Thread._set_tstate_lock(). * Add also threading._shutdown_locks_lock to protect access to threading._shutdown_locks. * Add test_finalization_shutdown() test. (cherry picked from commit 468e5fe) * bpo-36402: Fix threading.Thread._stop() (GH-14047) Remove the _tstate_lock from _shutdown_locks, don't remove None. (cherry picked from commit 6f75c87)
1 parent 032bf30 commit e40a97a

File tree

3 files changed

+120
-12
lines changed

3 files changed

+120
-12
lines changed

Lib/test/test_threading.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,41 @@ def __del__(self):
583583
self.assertEqual(data.splitlines(),
584584
["GC: True True True"] * 2)
585585

586+
def test_finalization_shutdown(self):
587+
# bpo-36402: Py_Finalize() calls threading._shutdown() which must wait
588+
# until Python thread states of all non-daemon threads get deleted.
589+
#
590+
# Test similar to SubinterpThreadingTests.test_threads_join_2(), but
591+
# test the finalization of the main interpreter.
592+
code = """if 1:
593+
import os
594+
import threading
595+
import time
596+
import random
597+
598+
def random_sleep():
599+
seconds = random.random() * 0.010
600+
time.sleep(seconds)
601+
602+
class Sleeper:
603+
def __del__(self):
604+
random_sleep()
605+
606+
tls = threading.local()
607+
608+
def f():
609+
# Sleep a bit so that the thread is still running when
610+
# Py_Finalize() is called.
611+
random_sleep()
612+
tls.x = Sleeper()
613+
random_sleep()
614+
615+
threading.Thread(target=f).start()
616+
random_sleep()
617+
"""
618+
rc, out, err = assert_python_ok("-c", code)
619+
self.assertEqual(err, b"")
620+
586621
def test_tstate_lock(self):
587622
# Test an implementation detail of Thread objects.
588623
started = _thread.allocate_lock()
@@ -703,6 +738,30 @@ def callback():
703738
finally:
704739
sys.settrace(old_trace)
705740

741+
@cpython_only
742+
def test_shutdown_locks(self):
743+
for daemon in (False, True):
744+
with self.subTest(daemon=daemon):
745+
event = threading.Event()
746+
thread = threading.Thread(target=event.wait, daemon=daemon)
747+
748+
# Thread.start() must add lock to _shutdown_locks,
749+
# but only for non-daemon thread
750+
thread.start()
751+
tstate_lock = thread._tstate_lock
752+
if not daemon:
753+
self.assertIn(tstate_lock, threading._shutdown_locks)
754+
else:
755+
self.assertNotIn(tstate_lock, threading._shutdown_locks)
756+
757+
# unblock the thread and join it
758+
event.set()
759+
thread.join()
760+
761+
# Thread._stop() must remove tstate_lock from _shutdown_locks.
762+
# Daemon threads must never add it to _shutdown_locks.
763+
self.assertNotIn(tstate_lock, threading._shutdown_locks)
764+
706765

707766
class ThreadJoinOnShutdown(BaseTestCase):
708767

@@ -878,15 +937,22 @@ def test_threads_join(self):
878937
self.addCleanup(os.close, w)
879938
code = r"""if 1:
880939
import os
940+
import random
881941
import threading
882942
import time
883943
944+
def random_sleep():
945+
seconds = random.random() * 0.010
946+
time.sleep(seconds)
947+
884948
def f():
885949
# Sleep a bit so that the thread is still running when
886950
# Py_EndInterpreter is called.
887-
time.sleep(0.05)
951+
random_sleep()
888952
os.write(%d, b"x")
953+
889954
threading.Thread(target=f).start()
955+
random_sleep()
890956
""" % (w,)
891957
ret = test.support.run_in_subinterp(code)
892958
self.assertEqual(ret, 0)
@@ -903,22 +969,29 @@ def test_threads_join_2(self):
903969
self.addCleanup(os.close, w)
904970
code = r"""if 1:
905971
import os
972+
import random
906973
import threading
907974
import time
908975
976+
def random_sleep():
977+
seconds = random.random() * 0.010
978+
time.sleep(seconds)
979+
909980
class Sleeper:
910981
def __del__(self):
911-
time.sleep(0.05)
982+
random_sleep()
912983
913984
tls = threading.local()
914985
915986
def f():
916987
# Sleep a bit so that the thread is still running when
917988
# Py_EndInterpreter is called.
918-
time.sleep(0.05)
989+
random_sleep()
919990
tls.x = Sleeper()
920991
os.write(%d, b"x")
992+
921993
threading.Thread(target=f).start()
994+
random_sleep()
922995
""" % (w,)
923996
ret = test.support.run_in_subinterp(code)
924997
self.assertEqual(ret, 0)

Lib/threading.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,11 @@ def _newname(template="Thread-%d"):
739739
_active = {} # maps thread id to Thread object
740740
_limbo = {}
741741
_dangling = WeakSet()
742+
# Set of Thread._tstate_lock locks of non-daemon threads used by _shutdown()
743+
# to wait until all Python thread states get deleted:
744+
# see Thread._set_tstate_lock().
745+
_shutdown_locks_lock = _allocate_lock()
746+
_shutdown_locks = set()
742747

743748
# Main class for threads
744749

@@ -903,6 +908,10 @@ def _set_tstate_lock(self):
903908
self._tstate_lock = _set_sentinel()
904909
self._tstate_lock.acquire()
905910

911+
if not self.daemon:
912+
with _shutdown_locks_lock:
913+
_shutdown_locks.add(self._tstate_lock)
914+
906915
def _bootstrap_inner(self):
907916
try:
908917
self._set_ident()
@@ -954,6 +963,9 @@ def _stop(self):
954963
assert not lock.locked()
955964
self._is_stopped = True
956965
self._tstate_lock = None
966+
if not self.daemon:
967+
with _shutdown_locks_lock:
968+
_shutdown_locks.discard(lock)
957969

958970
def _delete(self):
959971
"Remove current thread from the dict of currently running threads."
@@ -1342,6 +1354,9 @@ def enumerate():
13421354
_main_thread = _MainThread()
13431355

13441356
def _shutdown():
1357+
"""
1358+
Wait until the Python thread state of all non-daemon threads get deleted.
1359+
"""
13451360
# Obscure: other threads may be waiting to join _main_thread. That's
13461361
# dubious, but some code does it. We can't wait for C code to release
13471362
# the main thread's tstate_lock - that won't happen until the interpreter
@@ -1350,23 +1365,33 @@ def _shutdown():
13501365
if _main_thread._is_stopped:
13511366
# _shutdown() was already called
13521367
return
1368+
1369+
# Main thread
13531370
tlock = _main_thread._tstate_lock
13541371
# The main thread isn't finished yet, so its thread state lock can't have
13551372
# been released.
13561373
assert tlock is not None
13571374
assert tlock.locked()
13581375
tlock.release()
13591376
_main_thread._stop()
1360-
t = _pickSomeNonDaemonThread()
1361-
while t:
1362-
t.join()
1363-
t = _pickSomeNonDaemonThread()
13641377

1365-
def _pickSomeNonDaemonThread():
1366-
for t in enumerate():
1367-
if not t.daemon and t.is_alive():
1368-
return t
1369-
return None
1378+
# Join all non-deamon threads
1379+
while True:
1380+
with _shutdown_locks_lock:
1381+
locks = list(_shutdown_locks)
1382+
_shutdown_locks.clear()
1383+
1384+
if not locks:
1385+
break
1386+
1387+
for lock in locks:
1388+
# mimick Thread.join()
1389+
lock.acquire()
1390+
lock.release()
1391+
1392+
# new threads can be spawned while we were waiting for the other
1393+
# threads to complete
1394+
13701395

13711396
def main_thread():
13721397
"""Return the main thread object.
@@ -1392,12 +1417,18 @@ def _after_fork():
13921417
# Reset _active_limbo_lock, in case we forked while the lock was held
13931418
# by another (non-forked) thread. http://bugs.python.org/issue874900
13941419
global _active_limbo_lock, _main_thread
1420+
global _shutdown_locks_lock, _shutdown_locks
13951421
_active_limbo_lock = _allocate_lock()
13961422

13971423
# fork() only copied the current thread; clear references to others.
13981424
new_active = {}
13991425
current = current_thread()
14001426
_main_thread = current
1427+
1428+
# reset _shutdown() locks: threads re-register their _tstate_lock below
1429+
_shutdown_locks_lock = _allocate_lock()
1430+
_shutdown_locks = set()
1431+
14011432
with _active_limbo_lock:
14021433
# Dangling thread instances must still have their locks reset,
14031434
# because someone may join() them.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix a race condition at Python shutdown when waiting for threads. Wait until
2+
the Python thread state of all non-daemon threads get deleted (join all
3+
non-daemon threads), rather than just wait until non-daemon Python threads
4+
complete.

0 commit comments

Comments
 (0)