Skip to content

gh-127041: Prevent new threads after an interpreter has started finalizing #127044

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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ struct _is {
/* The linked list of threads, newest first. */
PyThreadState *head;
_PyThreadStateImpl *preallocated;
#define _PyInterpreterState_MAIN_SHUTTING_DOWN ((void *) 1)
/* The thread currently executing in the __main__ module, if any. */
PyThreadState *main;
/* Used in Modules/_threadmodule.c. */
Expand Down Expand Up @@ -405,6 +406,11 @@ PyAPI_FUNC(PyStatus) _PyInterpreterState_New(
PyThreadState *tstate,
PyInterpreterState **pinterp);

PyAPI_FUNC(int)
_PyInterpreterState_IsShuttingDown(PyInterpreterState *interp);

PyAPI_FUNC(int)
_PyInterpreterState_SetShuttingDown(PyInterpreterState *interp);

#define RARE_EVENT_INTERP_INC(interp, name) \
do { \
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_interpreters/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ def test_still_running(self):
interp.close()
self.assertTrue(interp.is_running())

@unittest.skipIf(True, "Wait for input on what to do about this test")
def test_subthreads_still_running(self):
r_interp, w_interp = self.pipe()
r_thread, w_thread = self.pipe()
Expand Down Expand Up @@ -595,6 +596,8 @@ def task():
t = threading.Thread(target=task)
t.start()
""")
# This now fails because destruction requires thread states
# to be inactive. Not sure what to do about that.
interp.close()

self.assertEqual(os.read(r_interp, 1), FINISHED)
Expand Down
27 changes: 27 additions & 0 deletions Lib/test/test_interpreters/test_stress.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ def run():
with threading_helper.start_threads(threads):
pass

def test_many_threads_running_and_destroying(self):
interp = interpreters.create()

def run():
try:
interp.exec("1")
interp.close()
except Exception as e:
# Ignore all interpreter errors, we just want to make
# sure that it doesn't crash
self.assertIsInstance(
e,
(
interpreters.ExecutionFailed,
interpreters.InterpreterNotFoundError,
interpreters.InterpreterError
)
)

threads = (threading.Thread(target=run) for _ in range(200))
with threading_helper.catch_threading_exception() as cm:
with threading_helper.start_threads(threads):
pass

self.assertIsNone(cm.exc_value)



if __name__ == '__main__':
# Test needs to be a package, so we can do relative imports.
Expand Down
20 changes: 12 additions & 8 deletions Modules/_interpretersmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -459,12 +459,13 @@ _run_in_interpreter(PyInterpreterState *interp,

// Prep and switch interpreters.
if (_PyXI_Enter(&session, interp, shareables) < 0) {
assert(!PyErr_Occurred());
PyObject *excinfo = _PyXI_ApplyError(session.error);
if (excinfo != NULL) {
*p_excinfo = excinfo;
if (!PyErr_Occurred()) {
PyObject *excinfo = _PyXI_ApplyError(session.error);
if (excinfo != NULL) {
*p_excinfo = excinfo;
}
assert(PyErr_Occurred());
}
assert(PyErr_Occurred());
return -1;
}

Expand Down Expand Up @@ -696,14 +697,17 @@ interp_destroy(PyObject *self, PyObject *args, PyObject *kwds)
return NULL;
}

// Ensure the interpreter isn't running.
// Ensure the interpreter isn't running, and won't ever run again.
/* XXX We *could* support destroying a running interpreter but
aren't going to worry about it for now. */
if (is_running_main(interp)) {
PyErr_Format(PyExc_InterpreterError, "interpreter running");
if (_PyInterpreterState_SetShuttingDown(interp) < 0) {
return NULL;
}

// Sanity checks
assert(_PyInterpreterState_IsShuttingDown(interp));
assert(!_PyInterpreterState_IsRunningMain(interp));

// Destroy the interpreter.
_PyXI_EndInterpreter(interp, NULL, NULL);

Expand Down
9 changes: 9 additions & 0 deletions Python/crossinterp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,15 @@ int
_PyXI_Enter(_PyXI_session *session,
PyInterpreterState *interp, PyObject *nsupdates)
{
if (_PyInterpreterState_IsShuttingDown(interp))
{
// This shouldn't be an error code because we want it
// to happen before we create a thread state
/* XXX Move to PyThreadState_New()? */
PyErr_SetString(PyExc_InterpreterError,
"interpreter is shutting down");
return -1;
}
// Convert the attrs for cross-interpreter use.
_PyXI_namespace *sharedns = NULL;
if (nsupdates != NULL) {
Expand Down
1 change: 1 addition & 0 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -2407,6 +2407,7 @@ void
Py_EndInterpreter(PyThreadState *tstate)
{
PyInterpreterState *interp = tstate->interp;
// XXX Mark the interpreter as shutting down here?

if (tstate != _PyThreadState_GET()) {
Py_FatalError("thread is not current");
Expand Down
47 changes: 45 additions & 2 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,47 @@ _PyErr_SetInterpreterAlreadyRunning(void)
PyErr_SetString(PyExc_InterpreterError, "interpreter already running");
}

int
_PyInterpreterState_IsShuttingDown(PyInterpreterState *interp)
{
return get_main_thread(interp) == _PyInterpreterState_MAIN_SHUTTING_DOWN;
}

int
_PyInterpreterState_SetShuttingDown(PyInterpreterState *interp)
{
void *expected = NULL;
// XXX Use an exchange without a comparison?
if (_Py_atomic_compare_exchange_ptr(&interp->threads.main,
&expected,
_PyInterpreterState_MAIN_SHUTTING_DOWN) == 0)
{
/* We're either already shutting down or another thread started running */
_PyErr_SetInterpreterAlreadyRunning();
return -1;
}
assert(_PyInterpreterState_IsShuttingDown(interp));
assert(!_PyInterpreterState_IsRunningMain(interp));
/* At this point, we're certain that no other threads can set the main thread.
* However, there might be some remaining thread states--in that case, let's just raise.
* We could wait for them all to finish up, but that's a job for later. */

// TODO: Switch this to the per-interpreter lock once Eric's PR is merged
HEAD_LOCK(interp->runtime);
PyThreadState *thread_head = interp->threads.head;
HEAD_UNLOCK(interp->runtime);
if (thread_head != NULL)
{
/* Remaining thread states exist */
PyErr_SetString(PyExc_InterpreterError, "interpreter has remaining threads");
set_main_thread(interp, NULL);
assert(!_PyInterpreterState_IsShuttingDown(interp));
return -1;
}

return 0;
}

int
_PyInterpreterState_SetRunningMain(PyInterpreterState *interp)
{
Expand All @@ -1071,6 +1112,7 @@ _PyInterpreterState_SetRunningMain(PyInterpreterState *interp)
"current tstate has wrong interpreter");
return -1;
}
assert(get_main_thread(interp) == NULL);
set_main_thread(interp, tstate);

return 0;
Expand All @@ -1086,8 +1128,9 @@ _PyInterpreterState_SetNotRunningMain(PyInterpreterState *interp)
int
_PyInterpreterState_IsRunningMain(PyInterpreterState *interp)
{
if (get_main_thread(interp) != NULL) {
return 1;
PyThreadState *main_thread = get_main_thread(interp);
if (main_thread != NULL) {
return main_thread != _PyInterpreterState_MAIN_SHUTTING_DOWN;
}
// Embedders might not know to call _PyInterpreterState_SetRunningMain(),
// so their main thread wouldn't show it is running the main interpreter's
Expand Down
Loading