Skip to content

gh-105716: Support Background Threads in Subinterpreters Consistently #109921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4e88c2b
Always wait for threads in subinterpreters.
ericsnowcurrently Sep 25, 2023
d08ac68
Add PyInterpreterState_*RunningMain().
ericsnowcurrently Sep 26, 2023
3b07482
Drop interpreter_exists().
ericsnowcurrently Sep 26, 2023
70c4753
Drop PyThreadState_IsRunning().
ericsnowcurrently Sep 26, 2023
678cb3b
Add a TODO.
ericsnowcurrently Sep 26, 2023
5be5a07
For now use the earliest thread state.
ericsnowcurrently Sep 26, 2023
81a1e0b
Add tests.
ericsnowcurrently Sep 26, 2023
c3f497b
Mark the main interpreter as running __main__.
ericsnowcurrently Sep 26, 2023
effa5b4
Call PyInterpreterState_SetNotRunningMain() in the right place.
ericsnowcurrently Sep 26, 2023
41c54b0
Check PyInterpreterState_IsRunningMain() preemptively, for now.
ericsnowcurrently Sep 26, 2023
5fbd940
Add docs.
ericsnowcurrently Sep 26, 2023
8b54c2e
Add a NEWS entry.
ericsnowcurrently Sep 26, 2023
5d1df61
Fix a typo.
ericsnowcurrently Sep 27, 2023
1034a67
Update the docs.
ericsnowcurrently Sep 27, 2023
d57729e
Update the docs.
ericsnowcurrently Sep 27, 2023
4219e2c
Update the docs.
ericsnowcurrently Sep 27, 2023
bfdace2
Drop a dead line.
ericsnowcurrently Sep 27, 2023
ef8af92
Drop an unused parameter.
ericsnowcurrently Sep 27, 2023
566cd08
Drop an unnecessary variable.
ericsnowcurrently Sep 27, 2023
d14b87a
Update TODO comments.
ericsnowcurrently Sep 27, 2023
1f2a321
Merge branch 'main' into subinterpreters-allow-background-threads
ericsnowcurrently Sep 27, 2023
55d7090
Merge branch 'main' into subinterpreters-allow-background-threads
ericsnowcurrently Sep 29, 2023
399859f
Do not expose the "Running" C-API (for now).
ericsnowcurrently Oct 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ struct _ts {
* if it is NULL. */
PyAPI_FUNC(PyThreadState *) _PyThreadState_UncheckedGet(void);


// Disable tracing and profiling.
PyAPI_FUNC(void) PyThreadState_EnterTracing(PyThreadState *tstate);

Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ struct _is {
uint64_t next_unique_id;
/* The linked list of threads, newest first. */
PyThreadState *head;
/* The thread currently executing in the __main__ module, if any. */
PyThreadState *main;
/* Used in Modules/_threadmodule.c. */
long count;
/* Support for runtime thread stack size tuning.
Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ _Py_IsMainInterpreterFinalizing(PyInterpreterState *interp)
interp == &_PyRuntime._main_interpreter);
}

// Export for _xxsubinterpreters module.
PyAPI_FUNC(int) _PyInterpreterState_SetRunningMain(PyInterpreterState *);
PyAPI_FUNC(void) _PyInterpreterState_SetNotRunningMain(PyInterpreterState *);
PyAPI_FUNC(int) _PyInterpreterState_IsRunningMain(PyInterpreterState *);


static inline const PyConfig *
_Py_GetMainConfig(void)
Expand Down
97 changes: 97 additions & 0 deletions Lib/test/test_interpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,16 @@ def test_subinterpreter(self):
self.assertTrue(interp.is_running())
self.assertFalse(interp.is_running())

def test_finished(self):
r, w = os.pipe()
interp = interpreters.create()
interp.run(f"""if True:
import os
os.write({w}, b'x')
""")
self.assertFalse(interp.is_running())
self.assertEqual(os.read(r, 1), b'x')

def test_from_subinterpreter(self):
interp = interpreters.create()
out = _run_output(interp, dedent(f"""
Expand Down Expand Up @@ -285,6 +295,31 @@ def test_bad_id(self):
with self.assertRaises(ValueError):
interp.is_running()

def test_with_only_background_threads(self):
r_interp, w_interp = os.pipe()
r_thread, w_thread = os.pipe()

DONE = b'D'
FINISHED = b'F'

interp = interpreters.create()
interp.run(f"""if True:
import os
import threading

def task():
v = os.read({r_thread}, 1)
assert v == {DONE!r}
os.write({w_interp}, {FINISHED!r})
t = threading.Thread(target=task)
t.start()
""")
self.assertFalse(interp.is_running())

os.write(w_thread, DONE)
interp.run('t.join()')
self.assertEqual(os.read(r_interp, 1), FINISHED)


class TestInterpreterClose(TestBase):

Expand Down Expand Up @@ -386,6 +421,37 @@ def test_still_running(self):
interp.close()
self.assertTrue(interp.is_running())

def test_subthreads_still_running(self):
r_interp, w_interp = os.pipe()
r_thread, w_thread = os.pipe()

FINISHED = b'F'

interp = interpreters.create()
interp.run(f"""if True:
import os
import threading
import time

done = False

def notify_fini():
global done
done = True
t.join()
threading._register_atexit(notify_fini)

def task():
while not done:
time.sleep(0.1)
os.write({w_interp}, {FINISHED!r})
t = threading.Thread(target=task)
t.start()
""")
interp.close()

self.assertEqual(os.read(r_interp, 1), FINISHED)


class TestInterpreterRun(TestBase):

Expand Down Expand Up @@ -462,6 +528,37 @@ def test_bytes_for_script(self):
with self.assertRaises(TypeError):
interp.run(b'print("spam")')

def test_with_background_threads_still_running(self):
r_interp, w_interp = os.pipe()
r_thread, w_thread = os.pipe()

RAN = b'R'
DONE = b'D'
FINISHED = b'F'

interp = interpreters.create()
interp.run(f"""if True:
import os
import threading

def task():
v = os.read({r_thread}, 1)
assert v == {DONE!r}
os.write({w_interp}, {FINISHED!r})
t = threading.Thread(target=task)
t.start()
os.write({w_interp}, {RAN!r})
""")
interp.run(f"""if True:
os.write({w_interp}, {RAN!r})
""")

os.write(w_thread, DONE)
interp.run('t.join()')
self.assertEqual(os.read(r_interp, 1), RAN)
self.assertEqual(os.read(r_interp, 1), RAN)
self.assertEqual(os.read(r_interp, 1), FINISHED)

# test_xxsubinterpreters covers the remaining Interpreter.run() behavior.


Expand Down
49 changes: 49 additions & 0 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
from test import lock_tests
from test import support

try:
from test.support import interpreters
except ModuleNotFoundError:
interpreters = None

threading_helper.requires_working_threading(module=True)

# Between fork() and exec(), only async-safe functions are allowed (issues
Expand All @@ -52,6 +57,12 @@ def skip_unless_reliable_fork(test):
return test


def requires_subinterpreters(meth):
"""Decorator to skip a test if subinterpreters are not supported."""
return unittest.skipIf(interpreters is None,
'subinterpreters required')(meth)


def restore_default_excepthook(testcase):
testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook)
threading.excepthook = threading.__excepthook__
Expand Down Expand Up @@ -1311,6 +1322,44 @@ def f():
# The thread was joined properly.
self.assertEqual(os.read(r, 1), b"x")

@requires_subinterpreters
def test_threads_join_with_no_main(self):
r_interp, w_interp = self.pipe()

INTERP = b'I'
FINI = b'F'
DONE = b'D'

interp = interpreters.create()
interp.run(f"""if True:
import os
import threading
import time

done = False

def notify_fini():
global done
done = True
os.write({w_interp}, {FINI!r})
t.join()
threading._register_atexit(notify_fini)

def task():
while not done:
time.sleep(0.1)
os.write({w_interp}, {DONE!r})
t = threading.Thread(target=task)
t.start()

os.write({w_interp}, {INTERP!r})
""")
interp.close()

self.assertEqual(os.read(r_interp, 1), INTERP)
self.assertEqual(os.read(r_interp, 1), FINI)
self.assertEqual(os.read(r_interp, 1), DONE)

@cpython_only
def test_daemon_threads_fatal_error(self):
subinterp_code = f"""if 1:
Expand Down
4 changes: 3 additions & 1 deletion Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
_allocate_lock = _thread.allocate_lock
_set_sentinel = _thread._set_sentinel
get_ident = _thread.get_ident
_is_main_interpreter = _thread._is_main_interpreter
try:
get_native_id = _thread.get_native_id
_HAVE_THREAD_NATIVE_ID = True
Expand Down Expand Up @@ -1574,7 +1575,7 @@ def _shutdown():
# the main thread's tstate_lock - that won't happen until the interpreter
# is nearly dead. So we release it here. Note that just calling _stop()
# isn't enough: other threads may already be waiting on _tstate_lock.
if _main_thread._is_stopped:
if _main_thread._is_stopped and _is_main_interpreter():
# _shutdown() was already called
return

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

# get thread-local implementation, either from the thread
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Subinterpreters now correctly handle the case where they have threads
running in the background. Before, such threads would interfere with
cleaning up and destroying them, as well as prevent running another script.
16 changes: 15 additions & 1 deletion Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1605,6 +1605,18 @@ PyDoc_STRVAR(excepthook_doc,
\n\
Handle uncaught Thread.run() exception.");

static PyObject *
thread__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored))
{
PyInterpreterState *interp = _PyInterpreterState_GET();
return PyBool_FromLong(_Py_IsMainInterpreter(interp));
}

PyDoc_STRVAR(thread__is_main_interpreter_doc,
"_is_main_interpreter()\n\
\n\
Return True if the current interpreter is the main Python interpreter.");

static PyMethodDef thread_methods[] = {
{"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread,
METH_VARARGS, start_new_doc},
Expand Down Expand Up @@ -1634,8 +1646,10 @@ static PyMethodDef thread_methods[] = {
METH_VARARGS, stack_size_doc},
{"_set_sentinel", thread__set_sentinel,
METH_NOARGS, _set_sentinel_doc},
{"_excepthook", thread_excepthook,
{"_excepthook", thread_excepthook,
METH_O, excepthook_doc},
{"_is_main_interpreter", thread__is_main_interpreter,
METH_NOARGS, thread__is_main_interpreter_doc},
{NULL, NULL} /* sentinel */
};

Expand Down
Loading