Skip to content

Commit cb5dae5

Browse files
ericsnowcurrentlyGlyphack
authored andcommitted
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.
1 parent b83e98c commit cb5dae5

File tree

11 files changed

+257
-45
lines changed

11 files changed

+257
-45
lines changed

Include/cpython/pystate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ struct _ts {
211211
* if it is NULL. */
212212
PyAPI_FUNC(PyThreadState *) _PyThreadState_UncheckedGet(void);
213213

214+
214215
// Disable tracing and profiling.
215216
PyAPI_FUNC(void) PyThreadState_EnterTracing(PyThreadState *tstate);
216217

Include/internal/pycore_interp.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ struct _is {
7373
uint64_t next_unique_id;
7474
/* The linked list of threads, newest first. */
7575
PyThreadState *head;
76+
/* The thread currently executing in the __main__ module, if any. */
77+
PyThreadState *main;
7678
/* Used in Modules/_threadmodule.c. */
7779
long count;
7880
/* 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
@@ -44,6 +44,11 @@ _Py_IsMainInterpreterFinalizing(PyInterpreterState *interp)
4444
interp == &_PyRuntime._main_interpreter);
4545
}
4646

47+
// Export for _xxsubinterpreters module.
48+
PyAPI_FUNC(int) _PyInterpreterState_SetRunningMain(PyInterpreterState *);
49+
PyAPI_FUNC(void) _PyInterpreterState_SetNotRunningMain(PyInterpreterState *);
50+
PyAPI_FUNC(int) _PyInterpreterState_IsRunningMain(PyInterpreterState *);
51+
4752

4853
static inline const PyConfig *
4954
_Py_GetMainConfig(void)

Lib/test/test_interpreters.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,16 @@ def test_subinterpreter(self):
261261
self.assertTrue(interp.is_running())
262262
self.assertFalse(interp.is_running())
263263

264+
def test_finished(self):
265+
r, w = os.pipe()
266+
interp = interpreters.create()
267+
interp.run(f"""if True:
268+
import os
269+
os.write({w}, b'x')
270+
""")
271+
self.assertFalse(interp.is_running())
272+
self.assertEqual(os.read(r, 1), b'x')
273+
264274
def test_from_subinterpreter(self):
265275
interp = interpreters.create()
266276
out = _run_output(interp, dedent(f"""
@@ -288,6 +298,31 @@ def test_bad_id(self):
288298
with self.assertRaises(ValueError):
289299
interp.is_running()
290300

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

292327
class TestInterpreterClose(TestBase):
293328

@@ -389,6 +424,37 @@ def test_still_running(self):
389424
interp.close()
390425
self.assertTrue(interp.is_running())
391426

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

393459
class TestInterpreterRun(TestBase):
394460

@@ -465,6 +531,37 @@ def test_bytes_for_script(self):
465531
with self.assertRaises(TypeError):
466532
interp.run(b'print("spam")')
467533

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

470567

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
@@ -52,6 +57,12 @@ def skip_unless_reliable_fork(test):
5257
return test
5358

5459

60+
def requires_subinterpreters(meth):
61+
"""Decorator to skip a test if subinterpreters are not supported."""
62+
return unittest.skipIf(interpreters is None,
63+
'subinterpreters required')(meth)
64+
65+
5566
def restore_default_excepthook(testcase):
5667
testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook)
5768
threading.excepthook = threading.__excepthook__
@@ -1311,6 +1322,44 @@ def f():
13111322
# The thread was joined properly.
13121323
self.assertEqual(os.read(r, 1), b"x")
13131324

1325+
@requires_subinterpreters
1326+
def test_threads_join_with_no_main(self):
1327+
r_interp, w_interp = self.pipe()
1328+
1329+
INTERP = b'I'
1330+
FINI = b'F'
1331+
DONE = b'D'
1332+
1333+
interp = interpreters.create()
1334+
interp.run(f"""if True:
1335+
import os
1336+
import threading
1337+
import time
1338+
1339+
done = False
1340+
1341+
def notify_fini():
1342+
global done
1343+
done = True
1344+
os.write({w_interp}, {FINI!r})
1345+
t.join()
1346+
threading._register_atexit(notify_fini)
1347+
1348+
def task():
1349+
while not done:
1350+
time.sleep(0.1)
1351+
os.write({w_interp}, {DONE!r})
1352+
t = threading.Thread(target=task)
1353+
t.start()
1354+
1355+
os.write({w_interp}, {INTERP!r})
1356+
""")
1357+
interp.close()
1358+
1359+
self.assertEqual(os.read(r_interp, 1), INTERP)
1360+
self.assertEqual(os.read(r_interp, 1), FINI)
1361+
self.assertEqual(os.read(r_interp, 1), DONE)
1362+
13141363
@cpython_only
13151364
def test_daemon_threads_fatal_error(self):
13161365
subinterp_code = f"""if 1:

Lib/threading.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
_allocate_lock = _thread.allocate_lock
3939
_set_sentinel = _thread._set_sentinel
4040
get_ident = _thread.get_ident
41+
_is_main_interpreter = _thread._is_main_interpreter
4142
try:
4243
get_native_id = _thread.get_native_id
4344
_HAVE_THREAD_NATIVE_ID = True
@@ -1574,7 +1575,7 @@ def _shutdown():
15741575
# the main thread's tstate_lock - that won't happen until the interpreter
15751576
# is nearly dead. So we release it here. Note that just calling _stop()
15761577
# isn't enough: other threads may already be waiting on _tstate_lock.
1577-
if _main_thread._is_stopped:
1578+
if _main_thread._is_stopped and _is_main_interpreter():
15781579
# _shutdown() was already called
15791580
return
15801581

@@ -1627,6 +1628,7 @@ def main_thread():
16271628
In normal conditions, the main thread is the thread from which the
16281629
Python interpreter was started.
16291630
"""
1631+
# XXX Figure this out for subinterpreters. (See gh-75698.)
16301632
return _main_thread
16311633

16321634
# 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
@@ -1605,6 +1605,18 @@ PyDoc_STRVAR(excepthook_doc,
16051605
\n\
16061606
Handle uncaught Thread.run() exception.");
16071607

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

0 commit comments

Comments
 (0)