Skip to content

Commit 8194653

Browse files
committed
Reproduce race in Thread.join()
1 parent 841eacd commit 8194653

File tree

2 files changed

+64
-0
lines changed

2 files changed

+64
-0
lines changed

Lib/threading.py

+17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import functools
77
import warnings
88

9+
import time
910
from time import monotonic as _time
1011
from _weakrefset import WeakSet
1112
from itertools import count as _count
@@ -1112,6 +1113,7 @@ def _stop(self):
11121113
# module's _shutdown() function.
11131114
lock = self._tstate_lock
11141115
if lock is not None:
1116+
self._log("Checking _tstate_lock is unlocked")
11151117
assert not lock.locked()
11161118
self._is_stopped = True
11171119
self._tstate_lock = None
@@ -1182,6 +1184,11 @@ def _join_os_thread(self):
11821184
self._handle = None
11831185
# No need to keep this around
11841186
self._join_lock = None
1187+
def _log(self, msg):
1188+
cur_thr_name = current_thread()._name
1189+
if cur_thr_name not in ("join-race-B", "join-race-C"):
1190+
return
1191+
print(f"[{current_thread()._name} join {self._name}] - {msg}")
11851192

11861193
def _wait_for_tstate_lock(self, block=True, timeout=-1):
11871194
# Issue #18808: wait for the thread state to be gone.
@@ -1196,9 +1203,19 @@ def _wait_for_tstate_lock(self, block=True, timeout=-1):
11961203
assert self._is_stopped
11971204
return
11981205

1206+
cur_thr_name = current_thread()._name
11991207
try:
12001208
if lock.acquire(block, timeout):
1209+
self._log(f"Acquired _tstate_lock for {self._name}")
1210+
if cur_thr_name =="join-race-C":
1211+
self._log("Sleeping for 2")
1212+
time.sleep(2)
1213+
self._log("Releasing _tstate_lock")
12011214
lock.release()
1215+
if cur_thr_name == "join-race-B":
1216+
self._log("Sleeping for 0.5")
1217+
time.sleep(0.5)
1218+
self._log("Calling _stop")
12021219
self._stop()
12031220
except:
12041221
if lock.locked():

repro_join_race.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import os
2+
import threading
3+
import traceback
4+
5+
6+
def waiter(event: threading.Event) -> None:
7+
event.wait()
8+
9+
10+
def joiner(thr: threading.Thread) -> None:
11+
try:
12+
thr.join()
13+
except AssertionError as exc:
14+
traceback.print_exc()
15+
os._exit(1)
16+
17+
18+
def repro() -> None:
19+
event = threading.Event()
20+
threads = []
21+
threads.append(threading.Thread(target=waiter, name="join-race-A", args=(event,)))
22+
threads.append(threading.Thread(target=joiner, name="join-race-B", args=(threads[0],)))
23+
threads.append(threading.Thread(target=joiner, name="join-race-C", args=(threads[0],)))
24+
for thr in threads:
25+
thr.start()
26+
# Unblock waiter
27+
event.set()
28+
29+
# Wait for joiners to exit. We must allow the joiner threads to wait first,
30+
# otherwise we may wait on the _tstate_lock first, acquire it, and set it
31+
# to None before either of the joiners have a chance to do so.
32+
threads[1].join()
33+
threads[2].join()
34+
35+
# Wait for waiter to exit
36+
threads[0].join()
37+
38+
39+
def main() -> None:
40+
for i in range(1000):
41+
print(f"=== Attempt {i} ===")
42+
repro()
43+
print()
44+
45+
46+
if __name__ == "__main__":
47+
main()

0 commit comments

Comments
 (0)