Skip to content

Commit e19af2c

Browse files
[3.12] pythongh-105716: Support Background Threads in Subinterpreters Consistently (pythongh-109921)
The existence of background threads running on a subinterpreter was preventing interpreters from getting properly destroyed, as well as impacting the ability to run the interpreter again. It also affected how we wait for non-daemon threads to finish. We add PyInterpreterState.threads.main, with some internal C-API functions. (cherry-picked from commit 1dd9dee)
1 parent 1ea4cb1 commit e19af2c

File tree

10 files changed

+262
-45
lines changed

10 files changed

+262
-45
lines changed

Include/internal/pycore_interp.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ struct _is {
6868
uint64_t next_unique_id;
6969
/* The linked list of threads, newest first. */
7070
PyThreadState *head;
71+
/* The thread currently executing in the __main__ module, if any. */
72+
PyThreadState *main;
7173
/* Used in Modules/_threadmodule.c. */
7274
long count;
7375
/* Support for runtime thread stack size tuning.

Include/internal/pycore_pystate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ _Py_IsMainInterpreterFinalizing(PyInterpreterState *interp)
4040
interp == &interp->runtime->_main_interpreter);
4141
}
4242

43+
// Export for _xxsubinterpreters module.
44+
PyAPI_FUNC(int) _PyInterpreterState_SetRunningMain(PyInterpreterState *);
45+
PyAPI_FUNC(void) _PyInterpreterState_SetNotRunningMain(PyInterpreterState *);
46+
PyAPI_FUNC(int) _PyInterpreterState_IsRunningMain(PyInterpreterState *);
47+
4348

4449
static inline const PyConfig *
4550
_Py_GetMainConfig(void)

Lib/test/test_interpreters.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,16 @@ def test_subinterpreter(self):
257257
self.assertTrue(interp.is_running())
258258
self.assertFalse(interp.is_running())
259259

260+
def test_finished(self):
261+
r, w = os.pipe()
262+
interp = interpreters.create()
263+
interp.run(f"""if True:
264+
import os
265+
os.write({w}, b'x')
266+
""")
267+
self.assertFalse(interp.is_running())
268+
self.assertEqual(os.read(r, 1), b'x')
269+
260270
def test_from_subinterpreter(self):
261271
interp = interpreters.create()
262272
out = _run_output(interp, dedent(f"""
@@ -284,6 +294,31 @@ def test_bad_id(self):
284294
with self.assertRaises(ValueError):
285295
interp.is_running()
286296

297+
def test_with_only_background_threads(self):
298+
r_interp, w_interp = os.pipe()
299+
r_thread, w_thread = os.pipe()
300+
301+
DONE = b'D'
302+
FINISHED = b'F'
303+
304+
interp = interpreters.create()
305+
interp.run(f"""if True:
306+
import os
307+
import threading
308+
309+
def task():
310+
v = os.read({r_thread}, 1)
311+
assert v == {DONE!r}
312+
os.write({w_interp}, {FINISHED!r})
313+
t = threading.Thread(target=task)
314+
t.start()
315+
""")
316+
self.assertFalse(interp.is_running())
317+
318+
os.write(w_thread, DONE)
319+
interp.run('t.join()')
320+
self.assertEqual(os.read(r_interp, 1), FINISHED)
321+
287322

288323
class TestInterpreterClose(TestBase):
289324

@@ -385,6 +420,37 @@ def test_still_running(self):
385420
interp.close()
386421
self.assertTrue(interp.is_running())
387422

423+
def test_subthreads_still_running(self):
424+
r_interp, w_interp = os.pipe()
425+
r_thread, w_thread = os.pipe()
426+
427+
FINISHED = b'F'
428+
429+
interp = interpreters.create()
430+
interp.run(f"""if True:
431+
import os
432+
import threading
433+
import time
434+
435+
done = False
436+
437+
def notify_fini():
438+
global done
439+
done = True
440+
t.join()
441+
threading._register_atexit(notify_fini)
442+
443+
def task():
444+
while not done:
445+
time.sleep(0.1)
446+
os.write({w_interp}, {FINISHED!r})
447+
t = threading.Thread(target=task)
448+
t.start()
449+
""")
450+
interp.close()
451+
452+
self.assertEqual(os.read(r_interp, 1), FINISHED)
453+
388454

389455
class TestInterpreterRun(TestBase):
390456

@@ -461,6 +527,37 @@ def test_bytes_for_script(self):
461527
with self.assertRaises(TypeError):
462528
interp.run(b'print("spam")')
463529

530+
def test_with_background_threads_still_running(self):
531+
r_interp, w_interp = os.pipe()
532+
r_thread, w_thread = os.pipe()
533+
534+
RAN = b'R'
535+
DONE = b'D'
536+
FINISHED = b'F'
537+
538+
interp = interpreters.create()
539+
interp.run(f"""if True:
540+
import os
541+
import threading
542+
543+
def task():
544+
v = os.read({r_thread}, 1)
545+
assert v == {DONE!r}
546+
os.write({w_interp}, {FINISHED!r})
547+
t = threading.Thread(target=task)
548+
t.start()
549+
os.write({w_interp}, {RAN!r})
550+
""")
551+
interp.run(f"""if True:
552+
os.write({w_interp}, {RAN!r})
553+
""")
554+
555+
os.write(w_thread, DONE)
556+
interp.run('t.join()')
557+
self.assertEqual(os.read(r_interp, 1), RAN)
558+
self.assertEqual(os.read(r_interp, 1), RAN)
559+
self.assertEqual(os.read(r_interp, 1), FINISHED)
560+
464561
# test_xxsubinterpreters covers the remaining Interpreter.run() behavior.
465562

466563

Lib/test/test_threading.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
from test import lock_tests
2727
from test import support
2828

29+
try:
30+
from test.support import interpreters
31+
except ModuleNotFoundError:
32+
interpreters = None
33+
2934
threading_helper.requires_working_threading(module=True)
3035

3136
# Between fork() and exec(), only async-safe functions are allowed (issues
@@ -45,6 +50,12 @@ def skip_unless_reliable_fork(test):
4550
return test
4651

4752

53+
def requires_subinterpreters(meth):
54+
"""Decorator to skip a test if subinterpreters are not supported."""
55+
return unittest.skipIf(interpreters is None,
56+
'subinterpreters required')(meth)
57+
58+
4859
def restore_default_excepthook(testcase):
4960
testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook)
5061
threading.excepthook = threading.__excepthook__
@@ -1296,6 +1307,44 @@ def f():
12961307
# The thread was joined properly.
12971308
self.assertEqual(os.read(r, 1), b"x")
12981309

1310+
@requires_subinterpreters
1311+
def test_threads_join_with_no_main(self):
1312+
r_interp, w_interp = self.pipe()
1313+
1314+
INTERP = b'I'
1315+
FINI = b'F'
1316+
DONE = b'D'
1317+
1318+
interp = interpreters.create()
1319+
interp.run(f"""if True:
1320+
import os
1321+
import threading
1322+
import time
1323+
1324+
done = False
1325+
1326+
def notify_fini():
1327+
global done
1328+
done = True
1329+
os.write({w_interp}, {FINI!r})
1330+
t.join()
1331+
threading._register_atexit(notify_fini)
1332+
1333+
def task():
1334+
while not done:
1335+
time.sleep(0.1)
1336+
os.write({w_interp}, {DONE!r})
1337+
t = threading.Thread(target=task)
1338+
t.start()
1339+
1340+
os.write({w_interp}, {INTERP!r})
1341+
""")
1342+
interp.close()
1343+
1344+
self.assertEqual(os.read(r_interp, 1), INTERP)
1345+
self.assertEqual(os.read(r_interp, 1), FINI)
1346+
self.assertEqual(os.read(r_interp, 1), DONE)
1347+
12991348
@cpython_only
13001349
def test_daemon_threads_fatal_error(self):
13011350
subinterp_code = f"""if 1:

Lib/threading.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
_allocate_lock = _thread.allocate_lock
3838
_set_sentinel = _thread._set_sentinel
3939
get_ident = _thread.get_ident
40+
_is_main_interpreter = _thread._is_main_interpreter
4041
try:
4142
get_native_id = _thread.get_native_id
4243
_HAVE_THREAD_NATIVE_ID = True
@@ -1566,7 +1567,7 @@ def _shutdown():
15661567
# the main thread's tstate_lock - that won't happen until the interpreter
15671568
# is nearly dead. So we release it here. Note that just calling _stop()
15681569
# isn't enough: other threads may already be waiting on _tstate_lock.
1569-
if _main_thread._is_stopped:
1570+
if _main_thread._is_stopped and _is_main_interpreter():
15701571
# _shutdown() was already called
15711572
return
15721573

@@ -1619,6 +1620,7 @@ def main_thread():
16191620
In normal conditions, the main thread is the thread from which the
16201621
Python interpreter was started.
16211622
"""
1623+
# XXX Figure this out for subinterpreters. (See gh-75698.)
16221624
return _main_thread
16231625

16241626
# get thread-local implementation, either from the thread
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Subinterpreters now correctly handle the case where they have threads
2+
running in the background. Before, such threads would interfere with
3+
cleaning up and destroying them, as well as prevent running another script.

Modules/_threadmodule.c

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,18 @@ PyDoc_STRVAR(excepthook_doc,
16041604
\n\
16051605
Handle uncaught Thread.run() exception.");
16061606

1607+
static PyObject *
1608+
thread__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored))
1609+
{
1610+
PyInterpreterState *interp = _PyInterpreterState_GET();
1611+
return PyBool_FromLong(_Py_IsMainInterpreter(interp));
1612+
}
1613+
1614+
PyDoc_STRVAR(thread__is_main_interpreter_doc,
1615+
"_is_main_interpreter()\n\
1616+
\n\
1617+
Return True if the current interpreter is the main Python interpreter.");
1618+
16071619
static PyMethodDef thread_methods[] = {
16081620
{"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread,
16091621
METH_VARARGS, start_new_doc},
@@ -1633,8 +1645,10 @@ static PyMethodDef thread_methods[] = {
16331645
METH_VARARGS, stack_size_doc},
16341646
{"_set_sentinel", thread__set_sentinel,
16351647
METH_NOARGS, _set_sentinel_doc},
1636-
{"_excepthook", thread_excepthook,
1648+
{"_excepthook", thread_excepthook,
16371649
METH_O, excepthook_doc},
1650+
{"_is_main_interpreter", thread__is_main_interpreter,
1651+
METH_NOARGS, thread__is_main_interpreter_doc},
16381652
{NULL, NULL} /* sentinel */
16391653
};
16401654

0 commit comments

Comments
 (0)