gh-113964: Don't prevent new threads until all non-daemon threads exit (#116677)

Starting in Python 3.12, we prevented calling fork() and starting new threads
during interpreter finalization (shutdown). This has led to a number of
regressions and flaky tests. We should not prevent starting new threads
(or `fork()`) until all non-daemon threads exit and finalization starts in
earnest.

This changes the checks to use `_PyInterpreterState_GetFinalizing(interp)`,
which is set immediately before terminating non-daemon threads.
This commit is contained in:
Sam Gross 2024-03-19 14:40:20 -04:00 committed by GitHub
parent 025ef7a5f7
commit 60e105c1c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 57 additions and 27 deletions

View File

@ -5357,20 +5357,21 @@ class ForkTests(unittest.TestCase):
self.assertEqual(err.decode("utf-8"), "") self.assertEqual(err.decode("utf-8"), "")
self.assertEqual(out.decode("utf-8"), "") self.assertEqual(out.decode("utf-8"), "")
def test_fork_at_exit(self): def test_fork_at_finalization(self):
code = """if 1: code = """if 1:
import atexit import atexit
import os import os
def exit_handler(): class AtFinalization:
pid = os.fork() def __del__(self):
if pid != 0: print("OK")
print("shouldn't be printed") pid = os.fork()
if pid != 0:
atexit.register(exit_handler) print("shouldn't be printed")
at_finalization = AtFinalization()
""" """
_, out, err = assert_python_ok("-c", code) _, out, err = assert_python_ok("-c", code)
self.assertEqual(b"", out) self.assertEqual(b"OK\n", out)
self.assertIn(b"can't fork at interpreter shutdown", err) self.assertIn(b"can't fork at interpreter shutdown", err)

View File

@ -3398,14 +3398,15 @@ class POSIXProcessTestCase(BaseTestCase):
def dummy(): def dummy():
pass pass
def exit_handler(): class AtFinalization:
subprocess.Popen({ZERO_RETURN_CMD}, preexec_fn=dummy) def __del__(self):
print("shouldn't be printed") print("OK")
subprocess.Popen({ZERO_RETURN_CMD}, preexec_fn=dummy)
atexit.register(exit_handler) print("shouldn't be printed")
at_finalization = AtFinalization()
""" """
_, out, err = assert_python_ok("-c", code) _, out, err = assert_python_ok("-c", code)
self.assertEqual(out, b'') self.assertEqual(out.strip(), b"OK")
self.assertIn(b"preexec_fn not supported at interpreter shutdown", err) self.assertIn(b"preexec_fn not supported at interpreter shutdown", err)
@unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"), @unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),

View File

@ -1154,21 +1154,21 @@ class ThreadTests(BaseTestCase):
self.assertEqual(out, b'') self.assertEqual(out, b'')
self.assertEqual(err, b'') self.assertEqual(err, b'')
def test_start_new_thread_at_exit(self): def test_start_new_thread_at_finalization(self):
code = """if 1: code = """if 1:
import atexit
import _thread import _thread
def f(): def f():
print("shouldn't be printed") print("shouldn't be printed")
def exit_handler(): class AtFinalization:
_thread.start_new_thread(f, ()) def __del__(self):
print("OK")
atexit.register(exit_handler) _thread.start_new_thread(f, ())
at_finalization = AtFinalization()
""" """
_, out, err = assert_python_ok("-c", code) _, out, err = assert_python_ok("-c", code)
self.assertEqual(out, b'') self.assertEqual(out.strip(), b"OK")
self.assertIn(b"can't create new thread at interpreter shutdown", err) self.assertIn(b"can't create new thread at interpreter shutdown", err)
class ThreadJoinOnShutdown(BaseTestCase): class ThreadJoinOnShutdown(BaseTestCase):
@ -1297,6 +1297,30 @@ class ThreadJoinOnShutdown(BaseTestCase):
rc, out, err = assert_python_ok('-c', script) rc, out, err = assert_python_ok('-c', script)
self.assertFalse(err) self.assertFalse(err)
def test_thread_from_thread(self):
script = """if True:
import threading
import time
def thread2():
time.sleep(0.05)
print("OK")
def thread1():
time.sleep(0.05)
t2 = threading.Thread(target=thread2)
t2.start()
t = threading.Thread(target=thread1)
t.start()
# do not join() -- the interpreter waits for non-daemon threads to
# finish.
"""
rc, out, err = assert_python_ok('-c', script)
self.assertEqual(err, b"")
self.assertEqual(out.strip(), b"OK")
self.assertEqual(rc, 0)
@skip_unless_reliable_fork @skip_unless_reliable_fork
def test_reinit_tls_after_fork(self): def test_reinit_tls_after_fork(self):
# Issue #13817: fork() would deadlock in a multithreaded program with # Issue #13817: fork() would deadlock in a multithreaded program with

View File

@ -0,0 +1,2 @@
Starting new threads and process creation through :func:`os.fork` are now
only prevented once all non-daemon threads exit.

View File

@ -1031,7 +1031,9 @@ subprocess_fork_exec_impl(PyObject *module, PyObject *process_args,
Py_ssize_t fds_to_keep_len = PyTuple_GET_SIZE(py_fds_to_keep); Py_ssize_t fds_to_keep_len = PyTuple_GET_SIZE(py_fds_to_keep);
PyInterpreterState *interp = _PyInterpreterState_GET(); PyInterpreterState *interp = _PyInterpreterState_GET();
if ((preexec_fn != Py_None) && interp->finalizing) { if ((preexec_fn != Py_None) &&
_PyInterpreterState_GetFinalizing(interp) != NULL)
{
PyErr_SetString(PyExc_PythonFinalizationError, PyErr_SetString(PyExc_PythonFinalizationError,
"preexec_fn not supported at interpreter shutdown"); "preexec_fn not supported at interpreter shutdown");
return NULL; return NULL;

View File

@ -1729,7 +1729,7 @@ do_start_new_thread(thread_module_state *state, PyObject *func, PyObject *args,
"thread is not supported for isolated subinterpreters"); "thread is not supported for isolated subinterpreters");
return -1; return -1;
} }
if (interp->finalizing) { if (_PyInterpreterState_GetFinalizing(interp) != NULL) {
PyErr_SetString(PyExc_PythonFinalizationError, PyErr_SetString(PyExc_PythonFinalizationError,
"can't create new thread at interpreter shutdown"); "can't create new thread at interpreter shutdown");
return -1; return -1;

View File

@ -7842,7 +7842,7 @@ os_fork1_impl(PyObject *module)
pid_t pid; pid_t pid;
PyInterpreterState *interp = _PyInterpreterState_GET(); PyInterpreterState *interp = _PyInterpreterState_GET();
if (interp->finalizing) { if (_PyInterpreterState_GetFinalizing(interp) != NULL) {
PyErr_SetString(PyExc_PythonFinalizationError, PyErr_SetString(PyExc_PythonFinalizationError,
"can't fork at interpreter shutdown"); "can't fork at interpreter shutdown");
return NULL; return NULL;
@ -7886,7 +7886,7 @@ os_fork_impl(PyObject *module)
{ {
pid_t pid; pid_t pid;
PyInterpreterState *interp = _PyInterpreterState_GET(); PyInterpreterState *interp = _PyInterpreterState_GET();
if (interp->finalizing) { if (_PyInterpreterState_GetFinalizing(interp) != NULL) {
PyErr_SetString(PyExc_PythonFinalizationError, PyErr_SetString(PyExc_PythonFinalizationError,
"can't fork at interpreter shutdown"); "can't fork at interpreter shutdown");
return NULL; return NULL;
@ -8719,7 +8719,7 @@ os_forkpty_impl(PyObject *module)
pid_t pid; pid_t pid;
PyInterpreterState *interp = _PyInterpreterState_GET(); PyInterpreterState *interp = _PyInterpreterState_GET();
if (interp->finalizing) { if (_PyInterpreterState_GetFinalizing(interp) != NULL) {
PyErr_SetString(PyExc_PythonFinalizationError, PyErr_SetString(PyExc_PythonFinalizationError,
"can't fork at interpreter shutdown"); "can't fork at interpreter shutdown");
return NULL; return NULL;

View File

@ -505,7 +505,7 @@ unicode_check_encoding_errors(const char *encoding, const char *errors)
/* Disable checks during Python finalization. For example, it allows to /* Disable checks during Python finalization. For example, it allows to
call _PyObject_Dump() during finalization for debugging purpose. */ call _PyObject_Dump() during finalization for debugging purpose. */
if (interp->finalizing) { if (_PyInterpreterState_GetFinalizing(interp) != NULL) {
return 0; return 0;
} }