gh-140746: Fix Thread.start() hanging when the thread dies during bootstrap#153776
Open
Jihaysse wants to merge 1 commit into
Open
gh-140746: Fix Thread.start() hanging when the thread dies during bootstrap#153776Jihaysse wants to merge 1 commit into
Jihaysse wants to merge 1 commit into
Conversation
…ng bootstrap If the new thread dies before _bootstrap_inner() reaches self._started.set() (e.g. a MemoryError raised while calling _bootstrap under memory pressure, or a silently-cleared SystemExit), the parent thread waits forever in Thread.start() and the dead thread lingers in threading._limbo. Add a C-level thread_started PyEvent to ThreadHandle, notified by the thread right after it sets _started and unconditionally by thread_run() on exit (covering a failed bootstrap call, SystemExit, and the finalization early exit). Thread.start() now waits on this event instead of the Python-level event alone and raises RuntimeError if the thread terminated before signalling startup.
Documentation build overview
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Thread.start()waits for the new thread with a bareself._started.wait(). If the new thread dies before_bootstrap_inner()reachesself._started.set(); typically aMemoryErrorraised while the C-levelthread_run()calls_bootstrapunder memory pressure (the interpreter printsException ignored in thread started by ...), or aSystemExitwhichthread_run()clears silently → the event is never set and the parent blocks forever. The dead thread also lingers inthreading._limbo, so it stays visible inthreading.enumerate()whileis_alive()isFalse.Any threaded server whose dispatcher thread spawns per-request threads (e.g.
socketserver.ThreadingMixIn) freezes permanently the first time this fires. This was detected in production on Python 3.12: workers stopped serving after a burst ofMemoryErrors, withkill -3stack dumps showing the dispatcher thread blocked inThread.start()→self._started.wait(), the request thread perpetually in its initial state, and no corresponding OS thread. Full investigation in gh-140746 (also reported by Patroni).Reproducer
Deterministic: simulates the
MemoryErrorthatthread_run()swallows while calling_bootstrap(the same C code path,PyObject_Call()returningNULL):Unpatched (reproduced on 3.12.11 and main): prints the "Exception ignored" traceback, then
start()blocks forever. With this PR:start()raisesRuntimeError: thread failed to startand the thread is removed from_limbo. The organic reproducer from the issue (progressively loweringRLIMIT_DATAuntil a realMemoryErrorlands in the thread's bootstrap) also hangs unpatched and fails cleanly with this PR after ~1300 progressively-starved thread starts.Fix
Add a C-level
thread_startedPyEventtoThreadHandle, mirroring the existingthread_is_exiting:_bootstrap_inner()notifies it (viahandle._set_started()) right afterself._started.set(), so a waiter woken by the child always observes_startedas set;thread_run()notifies it unconditionally on its exit path — covering a failedPyObject_Callof_bootstrap, the silentSystemExitcase, and the_PyThreadState_MustExit()early exit during finalization._PyEvent_Notify()is allocation-free C and a no-op on an already-set event, so this is safe on every exit path.Thread.start()waits on the C event (_wait_for_started(): GIL released, interruptible viaPy_MakePendingCalls()) and then checks_started: set means the thread started normally (identical behavior to today); unset means the thread died before starting and never will run, sostart()removes it from_limboand raisesRuntimeError("thread failed to start"). No timeout, no polling, no new handle states.Validation
Environment: Ubuntu 24.04 x86-64 (Linux 6.17), gcc 13.3.0; main built both
--with-pydebugand--disable-gil; stock 3.12.11 as the unpatched baseline.RuntimeError+_limbocleanup with this PR (both builds).ThreadingTCPServerwhose request thread dies in bootstrap freezes permanently unpatched; with this PR,handle_error()logs theRuntimeErrorand the server keeps serving.test_threading,test_thread,test_threadedtempfile,test_thread_local_bytecode,test_socketserver -u all,test_concurrent_futures.test_thread_pool,test_queue— all pass.MemoryErrorin_bootstrap,MemoryErrorin_bootstrap_inner, andSystemExitin_bootstrap.