Skip to content

Commit 2191497

Browse files
gh-136003: Execute pre-finalization callbacks in a loop (GH-136004)
1 parent d6a6fe2 commit 2191497

File tree

9 files changed

+281
-43
lines changed

9 files changed

+281
-43
lines changed

Include/cpython/pystate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ struct _ts {
107107
# define _PyThreadState_WHENCE_THREADING 3
108108
# define _PyThreadState_WHENCE_GILSTATE 4
109109
# define _PyThreadState_WHENCE_EXEC 5
110+
# define _PyThreadState_WHENCE_THREADING_DAEMON 6
110111
#endif
111112

112113
/* Currently holds the GIL. Must be its own field to avoid data races */

Lib/test/test_atexit.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,62 @@ def thready():
7979
# want them to affect the rest of the tests.
8080
script_helper.assert_python_ok("-c", textwrap.dedent(source))
8181

82+
@threading_helper.requires_working_threading()
83+
def test_thread_created_in_atexit(self):
84+
source = """if True:
85+
import atexit
86+
import threading
87+
import time
88+
89+
90+
def run():
91+
print(24)
92+
time.sleep(1)
93+
print(42)
94+
95+
@atexit.register
96+
def start_thread():
97+
threading.Thread(target=run).start()
98+
"""
99+
return_code, stdout, stderr = script_helper.assert_python_ok("-c", source)
100+
self.assertEqual(return_code, 0)
101+
self.assertEqual(stdout, f"24{os.linesep}42{os.linesep}".encode("utf-8"))
102+
self.assertEqual(stderr, b"")
103+
104+
@threading_helper.requires_working_threading()
105+
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
106+
def test_thread_created_in_atexit_subinterpreter(self):
107+
try:
108+
from concurrent import interpreters
109+
except ImportError:
110+
self.skipTest("subinterpreters are not available")
111+
112+
read, write = os.pipe()
113+
source = f"""if True:
114+
import atexit
115+
import threading
116+
import time
117+
import os
118+
119+
def run():
120+
os.write({write}, b'spanish')
121+
time.sleep(1)
122+
os.write({write}, b'inquisition')
123+
124+
@atexit.register
125+
def start_thread():
126+
threading.Thread(target=run).start()
127+
"""
128+
interp = interpreters.create()
129+
try:
130+
interp.exec(source)
131+
132+
# Close the interpreter to invoke atexit callbacks
133+
interp.close()
134+
self.assertEqual(os.read(read, 100), b"spanishinquisition")
135+
finally:
136+
os.close(read)
137+
os.close(write)
82138

83139
@support.cpython_only
84140
class SubinterpreterTest(unittest.TestCase):

Lib/test/test_capi/test_misc.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from test import support
2323
from test.support import MISSING_C_DOCSTRINGS
2424
from test.support import import_helper
25+
from test.support import script_helper
2526
from test.support import threading_helper
2627
from test.support import warnings_helper
2728
from test.support import requires_limited_api
@@ -1641,6 +1642,36 @@ def subthread():
16411642

16421643
self.assertEqual(actual, int(interpid))
16431644

1645+
@threading_helper.requires_working_threading()
1646+
def test_pending_call_creates_thread(self):
1647+
source = """
1648+
import _testinternalcapi
1649+
import threading
1650+
import time
1651+
1652+
1653+
def output():
1654+
print(24)
1655+
time.sleep(1)
1656+
print(42)
1657+
1658+
1659+
def callback():
1660+
threading.Thread(target=output).start()
1661+
1662+
1663+
def create_pending_call():
1664+
time.sleep(1)
1665+
_testinternalcapi.simple_pending_call(callback)
1666+
1667+
1668+
threading.Thread(target=create_pending_call).start()
1669+
"""
1670+
return_code, stdout, stderr = script_helper.assert_python_ok('-c', textwrap.dedent(source))
1671+
self.assertEqual(return_code, 0)
1672+
self.assertEqual(stdout, f"24{os.linesep}42{os.linesep}".encode("utf-8"))
1673+
self.assertEqual(stderr, b"")
1674+
16441675

16451676
class SubinterpreterTest(unittest.TestCase):
16461677

@@ -1949,6 +1980,41 @@ def test_module_state_shared_in_global(self):
19491980
subinterp_attr_id = os.read(r, 100)
19501981
self.assertEqual(main_attr_id, subinterp_attr_id)
19511982

1983+
@threading_helper.requires_working_threading()
1984+
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
1985+
@requires_subinterpreters
1986+
def test_pending_call_creates_thread_subinterpreter(self):
1987+
interpreters = import_helper.import_module("concurrent.interpreters")
1988+
r, w = os.pipe()
1989+
source = f"""if True:
1990+
import _testinternalcapi
1991+
import threading
1992+
import time
1993+
import os
1994+
1995+
1996+
def output():
1997+
time.sleep(1)
1998+
os.write({w}, b"x")
1999+
2000+
2001+
def callback():
2002+
threading.Thread(target=output).start()
2003+
2004+
2005+
def create_pending_call():
2006+
time.sleep(1)
2007+
_testinternalcapi.simple_pending_call(callback)
2008+
2009+
2010+
threading.Thread(target=create_pending_call).start()
2011+
"""
2012+
interp = interpreters.create()
2013+
interp.exec(source)
2014+
interp.close()
2015+
data = os.read(r, 1)
2016+
self.assertEqual(data, b"x")
2017+
19522018

19532019
@requires_subinterpreters
19542020
class InterpreterConfigTests(unittest.TestCase):

Lib/threading.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1557,8 +1557,9 @@ def _shutdown():
15571557
# normally - that won't happen until the interpreter is nearly dead. So
15581558
# mark it done here.
15591559
if _main_thread._os_thread_handle.is_done() and _is_main_interpreter():
1560-
# _shutdown() was already called
1561-
return
1560+
# _shutdown() was already called, but threads might have started
1561+
# in the meantime.
1562+
return _thread_shutdown()
15621563

15631564
global _SHUTTING_DOWN
15641565
_SHUTTING_DOWN = True
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :class:`threading.Thread` objects becoming incorrectly daemon when
2+
created from an :mod:`atexit` callback or a pending call
3+
(:c:func:`Py_AddPendingCall`).

Modules/_testinternalcapi.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2376,6 +2376,16 @@ emscripten_set_up_async_input_device(PyObject *self, PyObject *Py_UNUSED(ignored
23762376
}
23772377
#endif
23782378

2379+
static PyObject *
2380+
simple_pending_call(PyObject *self, PyObject *callable)
2381+
{
2382+
if (_PyEval_AddPendingCall(_PyInterpreterState_GET(), _pending_callback, Py_NewRef(callable), 0) < 0) {
2383+
return NULL;
2384+
}
2385+
2386+
Py_RETURN_NONE;
2387+
}
2388+
23792389
static PyMethodDef module_functions[] = {
23802390
{"get_configs", get_configs, METH_NOARGS},
23812391
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2481,6 +2491,7 @@ static PyMethodDef module_functions[] = {
24812491
#ifdef __EMSCRIPTEN__
24822492
{"emscripten_set_up_async_input_device", emscripten_set_up_async_input_device, METH_NOARGS},
24832493
#endif
2494+
{"simple_pending_call", simple_pending_call, METH_O},
24842495
{NULL, NULL} /* sentinel */
24852496
};
24862497

Modules/_threadmodule.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ force_done(void *arg)
429429

430430
static int
431431
ThreadHandle_start(ThreadHandle *self, PyObject *func, PyObject *args,
432-
PyObject *kwargs)
432+
PyObject *kwargs, int daemon)
433433
{
434434
// Mark the handle as starting to prevent any other threads from doing so
435435
PyMutex_Lock(&self->mutex);
@@ -453,7 +453,8 @@ ThreadHandle_start(ThreadHandle *self, PyObject *func, PyObject *args,
453453
goto start_failed;
454454
}
455455
PyInterpreterState *interp = _PyInterpreterState_GET();
456-
boot->tstate = _PyThreadState_New(interp, _PyThreadState_WHENCE_THREADING);
456+
uint8_t whence = daemon ? _PyThreadState_WHENCE_THREADING_DAEMON : _PyThreadState_WHENCE_THREADING;
457+
boot->tstate = _PyThreadState_New(interp, whence);
457458
if (boot->tstate == NULL) {
458459
PyMem_RawFree(boot);
459460
if (!PyErr_Occurred()) {
@@ -1916,7 +1917,7 @@ do_start_new_thread(thread_module_state *state, PyObject *func, PyObject *args,
19161917
add_to_shutdown_handles(state, handle);
19171918
}
19181919

1919-
if (ThreadHandle_start(handle, func, args, kwargs) < 0) {
1920+
if (ThreadHandle_start(handle, func, args, kwargs, daemon) < 0) {
19201921
if (!daemon) {
19211922
remove_from_shutdown_handles(handle);
19221923
}

0 commit comments

Comments
 (0)