Skip to content

Commit ff40ecd

Browse files
authored
bpo-31234: Add test.support.wait_threads_exit() (#3578)
Use _thread.count() to wait until threads exit. The new context manager prevents the "dangling thread" warning.
1 parent b8c7be2 commit ff40ecd

File tree

6 files changed

+161
-109
lines changed

6 files changed

+161
-109
lines changed

Lib/test/lock_tests.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ def __init__(self, f, n, wait_before_exit=False):
3131
self.started = []
3232
self.finished = []
3333
self._can_exit = not wait_before_exit
34+
self.wait_thread = support.wait_threads_exit()
35+
self.wait_thread.__enter__()
36+
3437
def task():
3538
tid = threading.get_ident()
3639
self.started.append(tid)
@@ -40,6 +43,7 @@ def task():
4043
self.finished.append(tid)
4144
while not self._can_exit:
4245
_wait()
46+
4347
try:
4448
for i in range(n):
4549
start_new_thread(task, ())
@@ -54,13 +58,8 @@ def wait_for_started(self):
5458
def wait_for_finished(self):
5559
while len(self.finished) < self.n:
5660
_wait()
57-
# Wait a little bit longer to prevent the "threading_cleanup()
58-
# failed to cleanup X threads" warning. The loop above is a weak
59-
# synchronization. At the C level, t_bootstrap() can still be
60-
# running and so _thread.count() still accounts the "almost dead"
61-
# thead.
62-
for _ in range(self.n):
63-
_wait()
61+
# Wait for threads exit
62+
self.wait_thread.__exit__(None, None, None)
6463

6564
def do_finish(self):
6665
self._can_exit = True
@@ -227,20 +226,23 @@ def test_reacquire(self):
227226
# Lock needs to be released before re-acquiring.
228227
lock = self.locktype()
229228
phase = []
229+
230230
def f():
231231
lock.acquire()
232232
phase.append(None)
233233
lock.acquire()
234234
phase.append(None)
235-
start_new_thread(f, ())
236-
while len(phase) == 0:
237-
_wait()
238-
_wait()
239-
self.assertEqual(len(phase), 1)
240-
lock.release()
241-
while len(phase) == 1:
235+
236+
with support.wait_threads_exit():
237+
start_new_thread(f, ())
238+
while len(phase) == 0:
239+
_wait()
242240
_wait()
243-
self.assertEqual(len(phase), 2)
241+
self.assertEqual(len(phase), 1)
242+
lock.release()
243+
while len(phase) == 1:
244+
_wait()
245+
self.assertEqual(len(phase), 2)
244246

245247
def test_different_thread(self):
246248
# Lock can be released from a different thread.

Lib/test/support/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2072,6 +2072,41 @@ def decorator(*args):
20722072
return decorator
20732073

20742074

2075+
@contextlib.contextmanager
2076+
def wait_threads_exit(timeout=60.0):
2077+
"""
2078+
bpo-31234: Context manager to wait until all threads created in the with
2079+
statement exit.
2080+
2081+
Use _thread.count() to check if threads exited. Indirectly, wait until
2082+
threads exit the internal t_bootstrap() C function of the _thread module.
2083+
2084+
threading_setup() and threading_cleanup() are designed to emit a warning
2085+
if a test leaves running threads in the background. This context manager
2086+
is designed to cleanup threads started by the _thread.start_new_thread()
2087+
which doesn't allow to wait for thread exit, whereas thread.Thread has a
2088+
join() method.
2089+
"""
2090+
old_count = _thread._count()
2091+
try:
2092+
yield
2093+
finally:
2094+
start_time = time.monotonic()
2095+
deadline = start_time + timeout
2096+
while True:
2097+
count = _thread._count()
2098+
if count <= old_count:
2099+
break
2100+
if time.monotonic() > deadline:
2101+
dt = time.monotonic() - start_time
2102+
msg = (f"wait_threads() failed to cleanup {count - old_count} "
2103+
f"threads after {dt:.1f} seconds "
2104+
f"(count: {count}, old count: {old_count})")
2105+
raise AssertionError(msg)
2106+
time.sleep(0.010)
2107+
gc_collect()
2108+
2109+
20752110
def reap_children():
20762111
"""Use this function at the end of test_main() whenever sub-processes
20772112
are started. This will help ensure that no extra children (zombies)

Lib/test/test_socket.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,9 @@ def serverExplicitReady(self):
271271
self.server_ready.set()
272272

273273
def _setUp(self):
274+
self.wait_threads = support.wait_threads_exit()
275+
self.wait_threads.__enter__()
276+
274277
self.server_ready = threading.Event()
275278
self.client_ready = threading.Event()
276279
self.done = threading.Event()
@@ -297,6 +300,7 @@ def _setUp(self):
297300
def _tearDown(self):
298301
self.__tearDown()
299302
self.done.wait()
303+
self.wait_threads.__exit__(None, None, None)
300304

301305
if self.queue.qsize():
302306
exc = self.queue.get()

Lib/test/test_thread.py

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,13 @@ def task(self, ident):
5959
self.done_mutex.release()
6060

6161
def test_starting_threads(self):
62-
# Basic test for thread creation.
63-
for i in range(NUMTASKS):
64-
self.newtask()
65-
verbose_print("waiting for tasks to complete...")
66-
self.done_mutex.acquire()
67-
verbose_print("all tasks done")
62+
with support.wait_threads_exit():
63+
# Basic test for thread creation.
64+
for i in range(NUMTASKS):
65+
self.newtask()
66+
verbose_print("waiting for tasks to complete...")
67+
self.done_mutex.acquire()
68+
verbose_print("all tasks done")
6869

6970
def test_stack_size(self):
7071
# Various stack size tests.
@@ -94,12 +95,13 @@ def test_nt_and_posix_stack_size(self):
9495
verbose_print("trying stack_size = (%d)" % tss)
9596
self.next_ident = 0
9697
self.created = 0
97-
for i in range(NUMTASKS):
98-
self.newtask()
98+
with support.wait_threads_exit():
99+
for i in range(NUMTASKS):
100+
self.newtask()
99101

100-
verbose_print("waiting for all tasks to complete")
101-
self.done_mutex.acquire()
102-
verbose_print("all tasks done")
102+
verbose_print("waiting for all tasks to complete")
103+
self.done_mutex.acquire()
104+
verbose_print("all tasks done")
103105

104106
thread.stack_size(0)
105107

@@ -109,25 +111,28 @@ def test__count(self):
109111
mut = thread.allocate_lock()
110112
mut.acquire()
111113
started = []
114+
112115
def task():
113116
started.append(None)
114117
mut.acquire()
115118
mut.release()
116-
thread.start_new_thread(task, ())
117-
while not started:
118-
time.sleep(POLL_SLEEP)
119-
self.assertEqual(thread._count(), orig + 1)
120-
# Allow the task to finish.
121-
mut.release()
122-
# The only reliable way to be sure that the thread ended from the
123-
# interpreter's point of view is to wait for the function object to be
124-
# destroyed.
125-
done = []
126-
wr = weakref.ref(task, lambda _: done.append(None))
127-
del task
128-
while not done:
129-
time.sleep(POLL_SLEEP)
130-
self.assertEqual(thread._count(), orig)
119+
120+
with support.wait_threads_exit():
121+
thread.start_new_thread(task, ())
122+
while not started:
123+
time.sleep(POLL_SLEEP)
124+
self.assertEqual(thread._count(), orig + 1)
125+
# Allow the task to finish.
126+
mut.release()
127+
# The only reliable way to be sure that the thread ended from the
128+
# interpreter's point of view is to wait for the function object to be
129+
# destroyed.
130+
done = []
131+
wr = weakref.ref(task, lambda _: done.append(None))
132+
del task
133+
while not done:
134+
time.sleep(POLL_SLEEP)
135+
self.assertEqual(thread._count(), orig)
131136

132137
def test_save_exception_state_on_error(self):
133138
# See issue #14474
@@ -140,16 +145,14 @@ def mywrite(self, *args):
140145
except ValueError:
141146
pass
142147
real_write(self, *args)
143-
c = thread._count()
144148
started = thread.allocate_lock()
145149
with support.captured_output("stderr") as stderr:
146150
real_write = stderr.write
147151
stderr.write = mywrite
148152
started.acquire()
149-
thread.start_new_thread(task, ())
150-
started.acquire()
151-
while thread._count() > c:
152-
time.sleep(POLL_SLEEP)
153+
with support.wait_threads_exit():
154+
thread.start_new_thread(task, ())
155+
started.acquire()
153156
self.assertIn("Traceback", stderr.getvalue())
154157

155158

@@ -181,13 +184,14 @@ def enter(self):
181184
class BarrierTest(BasicThreadTest):
182185

183186
def test_barrier(self):
184-
self.bar = Barrier(NUMTASKS)
185-
self.running = NUMTASKS
186-
for i in range(NUMTASKS):
187-
thread.start_new_thread(self.task2, (i,))
188-
verbose_print("waiting for tasks to end")
189-
self.done_mutex.acquire()
190-
verbose_print("tasks done")
187+
with support.wait_threads_exit():
188+
self.bar = Barrier(NUMTASKS)
189+
self.running = NUMTASKS
190+
for i in range(NUMTASKS):
191+
thread.start_new_thread(self.task2, (i,))
192+
verbose_print("waiting for tasks to end")
193+
self.done_mutex.acquire()
194+
verbose_print("tasks done")
191195

192196
def task2(self, ident):
193197
for i in range(NUMTRIPS):
@@ -225,11 +229,10 @@ def setUp(self):
225229
@unittest.skipUnless(hasattr(os, 'fork'), 'need os.fork')
226230
@support.reap_threads
227231
def test_forkinthread(self):
228-
running = True
229232
status = "not set"
230233

231234
def thread1():
232-
nonlocal running, status
235+
nonlocal status
233236

234237
# fork in a thread
235238
pid = os.fork()
@@ -244,13 +247,11 @@ def thread1():
244247
# parent
245248
os.close(self.write_fd)
246249
pid, status = os.waitpid(pid, 0)
247-
running = False
248250

249-
thread.start_new_thread(thread1, ())
250-
self.assertEqual(os.read(self.read_fd, 2), b"OK",
251-
"Unable to fork() in thread")
252-
while running:
253-
time.sleep(POLL_SLEEP)
251+
with support.wait_threads_exit():
252+
thread.start_new_thread(thread1, ())
253+
self.assertEqual(os.read(self.read_fd, 2), b"OK",
254+
"Unable to fork() in thread")
254255
self.assertEqual(status, 0)
255256

256257
def tearDown(self):

Lib/test/test_threading.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,10 @@ def f():
125125
done.set()
126126
done = threading.Event()
127127
ident = []
128-
_thread.start_new_thread(f, ())
129-
done.wait()
130-
self.assertIsNotNone(ident[0])
128+
with support.wait_threads_exit():
129+
tid = _thread.start_new_thread(f, ())
130+
done.wait()
131+
self.assertEqual(ident[0], tid)
131132
# Kill the "immortal" _DummyThread
132133
del threading._active[ident[0]]
133134

@@ -165,9 +166,10 @@ def f(mutex):
165166

166167
mutex = threading.Lock()
167168
mutex.acquire()
168-
tid = _thread.start_new_thread(f, (mutex,))
169-
# Wait for the thread to finish.
170-
mutex.acquire()
169+
with support.wait_threads_exit():
170+
tid = _thread.start_new_thread(f, (mutex,))
171+
# Wait for the thread to finish.
172+
mutex.acquire()
171173
self.assertIn(tid, threading._active)
172174
self.assertIsInstance(threading._active[tid], threading._DummyThread)
173175
#Issue 29376

0 commit comments

Comments
 (0)