Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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_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;

Check warning on line 1087 in Python/pystate.c

View workflow job for this annotation

GitHub Actions / Address sanitizer (ubuntu-24.04)

unused variable ‘thread_head’ [-Wunused-variable]

Check warning on line 1087 in Python/pystate.c

View workflow job for this annotation

GitHub Actions / Hypothesis tests on Ubuntu

unused variable ‘thread_head’ [-Wunused-variable]

Check warning on line 1087 in Python/pystate.c

View workflow job for this annotation

GitHub Actions / Ubuntu / build and test (ubuntu-24.04)

unused variable ‘thread_head’ [-Wunused-variable]

Check warning on line 1087 in Python/pystate.c

View workflow job for this annotation

GitHub Actions / Ubuntu (free-threading) / build and test (ubuntu-24.04)

unused variable ‘thread_head’ [-Wunused-variable]
HEAD_UNLOCK(interp->runtime);
if (interp->threads.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 @@
"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 @@
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