Skip to content

gh-116612: Fork before shutdown in test_3_join_in_forked_from_thread - #158130

Open
v0ropaev wants to merge 2 commits into
python:mainfrom
v0ropaev:gh-116612-join-forked-thread
Open

v0ropaev wants to merge 2 commits into
python:mainfrom
v0ropaev:gh-116612-join-forked-thread

Conversation

@v0ropaev

@v0ropaev v0ropaev commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Closes #116612.

test_3_join_in_forked_from_thread forks from a worker thread while the main thread is
running off the end of the script, so whether the fork lands before or after interpreter
shutdown begins is a race. When it lands after, the main thread is already marked stopped in
the parent, the child inherits that, and mainthread.join() in the child returns for the
wrong reason, so the test passes without testing anything.

Two lines of substance in Lib/test/test_threading.py. A w.join() at the end of the script
keeps the main thread alive until the worker has forked, which is what actually establishes
the ordering. And a check in the worker that the main thread is still alive immediately
before fork(), exiting 3 and printing to stderr if it is not, which is what makes a future
regression loud instead of silently green.

Worth being upfront about one thing. The race does not reproduce unaided on this machine.
With w.join() removed the test still passed 12 runs out of 12, the fork wins every time
here, so there is no red to green transition to show. Delaying the fork by 0.5s inside the
worker is enough to bring it out, which is what a loaded machine or a slower interpreter does
for free. With w.join() removed and that delay in place it fails with
main thread stopped before fork(); with w.join() present and the same delay it passes. So
the main thread really can be gone by the time the worker forks, w.join() is what prevents
it, and without the guard that state would have gone unnoticed.

./python.exe -m test test_threading passes.

…hread

The test script ended at ``w.start()``, so the ``os.fork()`` in
``worker()`` raced the interpreter shutdown that begins as soon as the
main thread falls off the end of the script.

When the fork loses that race it happens inside
``threading._shutdown()``, which marks the main thread as stopped
before joining non-daemon threads.  The child then inherits a main
thread that was already marked as stopped in the parent, so
``mainthread.join()`` in ``joiningfunc()`` returns for the wrong reason
and the test stops checking what its own comment describes: "In the
forked process, the main Thread object must be marked as stopped."

Join ``w`` so that the main thread stays alive until the fork has
happened, and check in ``worker()`` that the main thread really is
still alive before forking.  Without that check the test passes either
way, so losing the join again would go unnoticed.

With ``time.sleep(0.1)`` added at the start of ``worker()`` (the
reproducer from the issue), the fork previously saw
``threading._SHUTTING_DOWN`` true and the main thread already marked
done; it now sees both false, and the main thread handle is marked
done only in the child.
@bedevere-app

bedevere-app Bot commented Sep 24, 2026

Copy link
Copy Markdown

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@v0ropaev
v0ropaev force-pushed the gh-116612-join-forked-thread branch from 05190d2 to 9ef91ca Compare September 28, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review tests Tests in the Lib/test dir

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test_3_join_in_forked_from_thread forks (and starts thread) at shutdown

1 participant