Skip to content
15 changes: 15 additions & 0 deletions Lib/test/test_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,21 @@ def fork_thread(read_fd, write_fd):
self.assertIsNotNone(pid)
support.wait_process(pid, exitcode=0)

@support.requires_fork()
@threading_helper.reap_threads
def test_forked_not_started(self):
handle = thread._ThreadHandle()
pid = os.fork()
if pid == 0:
# child process
try:
self.assertTrue(handle.is_alive())
self.assertFalse(handle.is_done())
finally:
os._exit(0)
else:
support.wait_process(pid, exitcode=0)

def tearDown(self):
try:
os.close(self.read_fd)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix :exc:`RuntimeError` when using a not-started :class:`threading.Thread` after calling :func:`os.fork`
5 changes: 5 additions & 0 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ _PyThread_AfterFork(struct _pythread_runtime_state *state)
continue;
}

// keep not_start handles – they are safe to start in the child
if (handle->state == THREAD_HANDLE_NOT_STARTED){
continue;
}

// Mark all threads as done. Any attempts to join or detach the
// underlying OS thread (if any) could crash. We are the only thread;
// it's safe to set this non-atomically.
Expand Down
Loading