Skip to content

Commit 93d0581

Browse files
fix: Clean up refused native thread starts
1 parent f7c39d2 commit 93d0581

2 files changed

Lines changed: 68 additions & 16 deletions

File tree

‎Lib/test/test_threading.py‎

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -463,20 +463,70 @@ def catch_exception():
463463

464464
self.assertIsInstance(caught_exception, InjectedException)
465465

466+
@requires_subprocess()
466467
def test_limbo_cleanup(self):
467-
# Issue 7481: Failure to start thread should cleanup the limbo map.
468-
def fail_new_thread(*args, **kwargs):
469-
raise threading.ThreadError()
470-
_start_joinable_thread = threading._start_joinable_thread
471-
threading._start_joinable_thread = fail_new_thread
472-
try:
473-
t = threading.Thread(target=lambda: None)
474-
self.assertRaises(threading.ThreadError, t.start)
475-
self.assertFalse(
476-
t in threading._limbo,
477-
"Failed to cleanup _limbo map on failure of Thread.start().")
478-
finally:
479-
threading._start_joinable_thread = _start_joinable_thread
468+
# Issue 7481: Failure to start a thread must clean up the limbo map.
469+
code = """if 1:
470+
import builtins
471+
import sys
472+
import threading
473+
474+
error = getattr(builtins, sys.argv[1])()
475+
def audit(event, args):
476+
if event == '_thread.start_joinable_thread':
477+
raise error
478+
479+
sys.addaudithook(audit)
480+
thread = threading.Thread()
481+
try:
482+
thread.start()
483+
except BaseException as caught:
484+
assert caught is error
485+
else:
486+
raise AssertionError('Thread.start() did not raise')
487+
488+
assert thread._os_thread_handle.ident == 0
489+
assert not thread._started.is_set()
490+
assert thread not in threading._limbo
491+
assert thread not in threading.enumerate()
492+
"""
493+
for exception in ('RuntimeError', 'KeyboardInterrupt', 'SystemExit'):
494+
with self.subTest(exception=exception):
495+
assert_python_ok('-c', code, exception)
496+
497+
@requires_subprocess()
498+
def test_start_interrupted_after_native_start(self):
499+
code = """if 1:
500+
import sys
501+
import threading
502+
from test.support import SHORT_TIMEOUT, threading_helper
503+
504+
def interrupt(frame, event, arg):
505+
if event == 'c_return' and arg is threading._start_joinable_thread:
506+
raise KeyboardInterrupt
507+
508+
completed = threading.Event()
509+
thread = threading.Thread(target=completed.set)
510+
# Hold the child before it signals successful startup.
511+
with thread._started._cond:
512+
sys.setprofile(interrupt)
513+
try:
514+
thread.start()
515+
except KeyboardInterrupt:
516+
pass
517+
else:
518+
raise AssertionError('Thread.start() did not raise')
519+
finally:
520+
sys.setprofile(None)
521+
assert thread._os_thread_handle.ident
522+
assert thread in threading._limbo
523+
524+
assert thread._started.wait(SHORT_TIMEOUT)
525+
threading_helper.join_thread(thread)
526+
assert completed.is_set()
527+
"""
528+
_, _, stderr = assert_python_ok('-c', code)
529+
self.assertEqual(stderr, b'')
480530

481531
def test_finalize_running_thread(self):
482532
# Issue 1402: the PyGILState_Ensure / _Release functions may be called

‎Lib/threading.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,9 +1141,11 @@ def start(self):
11411141
# Start joinable thread
11421142
_start_joinable_thread(self._bootstrap, handle=self._os_thread_handle,
11431143
daemon=self.daemon)
1144-
except Exception:
1145-
with _active_limbo_lock:
1146-
del _limbo[self]
1144+
except BaseException:
1145+
# The native call can be interrupted after creating the OS thread.
1146+
if not self._os_thread_handle.ident:
1147+
with _active_limbo_lock:
1148+
del _limbo[self]
11471149
raise
11481150
self._started.wait() # Will set ident and native_id
11491151

0 commit comments

Comments
 (0)