mirror of
https://github.com/python/cpython.git
synced 2025-10-25 18:54:53 +00:00
[3.14] gh-128639: Don't assume one thread in subinterpreter finalization with fixed daemon thread support (GH-134606) (GH-139050)
gh-128639: Don't assume one thread in subinterpreter finalization with fixed daemon thread support (GH-134606)
This reapplies GH-128640.
(cherry picked from commit a64881363b)
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
This commit is contained in:
parent
08bea299bf
commit
cec4ddf23e
6 changed files with 114 additions and 39 deletions
|
|
@ -11,6 +11,7 @@
|
||||||
from test.support import os_helper
|
from test.support import os_helper
|
||||||
from test.support import script_helper
|
from test.support import script_helper
|
||||||
from test.support import import_helper
|
from test.support import import_helper
|
||||||
|
from test.support.script_helper import assert_python_ok
|
||||||
# Raise SkipTest if subinterpreters not supported.
|
# Raise SkipTest if subinterpreters not supported.
|
||||||
_interpreters = import_helper.import_module('_interpreters')
|
_interpreters = import_helper.import_module('_interpreters')
|
||||||
from concurrent import interpreters
|
from concurrent import interpreters
|
||||||
|
|
@ -707,6 +708,68 @@ def test_created_with_capi(self):
|
||||||
self.interp_exists(interpid))
|
self.interp_exists(interpid))
|
||||||
|
|
||||||
|
|
||||||
|
def test_remaining_threads(self):
|
||||||
|
r_interp, w_interp = self.pipe()
|
||||||
|
|
||||||
|
FINISHED = b'F'
|
||||||
|
|
||||||
|
# It's unlikely, but technically speaking, it's possible
|
||||||
|
# that the thread could've finished before interp.close() is
|
||||||
|
# reached, so this test might not properly exercise the case.
|
||||||
|
# However, it's quite unlikely and probably not worth bothering about.
|
||||||
|
interp = interpreters.create()
|
||||||
|
interp.exec(f"""if True:
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
def task():
|
||||||
|
time.sleep(1)
|
||||||
|
os.write({w_interp}, {FINISHED!r})
|
||||||
|
|
||||||
|
threads = (threading.Thread(target=task) for _ in range(3))
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
""")
|
||||||
|
interp.close()
|
||||||
|
|
||||||
|
self.assertEqual(os.read(r_interp, 1), FINISHED)
|
||||||
|
|
||||||
|
def test_remaining_daemon_threads(self):
|
||||||
|
# Daemon threads leak reference by nature, because they hang threads
|
||||||
|
# without allowing them to do cleanup (i.e., release refs).
|
||||||
|
# To prevent that from messing up the refleak hunter and whatnot, we
|
||||||
|
# run this in a subprocess.
|
||||||
|
code = '''if True:
|
||||||
|
import _interpreters
|
||||||
|
import types
|
||||||
|
interp = _interpreters.create(
|
||||||
|
types.SimpleNamespace(
|
||||||
|
use_main_obmalloc=False,
|
||||||
|
allow_fork=False,
|
||||||
|
allow_exec=False,
|
||||||
|
allow_threads=True,
|
||||||
|
allow_daemon_threads=True,
|
||||||
|
check_multi_interp_extensions=True,
|
||||||
|
gil='own',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_interpreters.exec(interp, f"""if True:
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
def task():
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
threads = (threading.Thread(target=task, daemon=True) for _ in range(3))
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
""")
|
||||||
|
_interpreters.destroy(interp)
|
||||||
|
'''
|
||||||
|
assert_python_ok('-c', code)
|
||||||
|
|
||||||
|
|
||||||
class TestInterpreterPrepareMain(TestBase):
|
class TestInterpreterPrepareMain(TestBase):
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
|
|
@ -815,7 +878,10 @@ def script():
|
||||||
spam.eggs()
|
spam.eggs()
|
||||||
|
|
||||||
interp = interpreters.create()
|
interp = interpreters.create()
|
||||||
interp.exec(script)
|
try:
|
||||||
|
interp.exec(script)
|
||||||
|
finally:
|
||||||
|
interp.close()
|
||||||
""")
|
""")
|
||||||
|
|
||||||
stdout, stderr = self.assert_python_failure(scriptfile)
|
stdout, stderr = self.assert_python_failure(scriptfile)
|
||||||
|
|
@ -824,7 +890,7 @@ def script():
|
||||||
# File "{interpreters.__file__}", line 179, in exec
|
# File "{interpreters.__file__}", line 179, in exec
|
||||||
self.assertEqual(stderr, dedent(f"""\
|
self.assertEqual(stderr, dedent(f"""\
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
File "{scriptfile}", line 9, in <module>
|
File "{scriptfile}", line 10, in <module>
|
||||||
interp.exec(script)
|
interp.exec(script)
|
||||||
~~~~~~~~~~~^^^^^^^^
|
~~~~~~~~~~~^^^^^^^^
|
||||||
{interpmod_line.strip()}
|
{interpmod_line.strip()}
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ def test_sys_path_0(self):
|
||||||
'sub': sys.path[0],
|
'sub': sys.path[0],
|
||||||
}}, indent=4), flush=True)
|
}}, indent=4), flush=True)
|
||||||
""")
|
""")
|
||||||
|
interp.close()
|
||||||
'''
|
'''
|
||||||
# <tmp>/
|
# <tmp>/
|
||||||
# pkg/
|
# pkg/
|
||||||
|
|
@ -172,7 +173,10 @@ def test_gh_109793(self):
|
||||||
argv = [sys.executable, '-c', '''if True:
|
argv = [sys.executable, '-c', '''if True:
|
||||||
from concurrent import interpreters
|
from concurrent import interpreters
|
||||||
interp = interpreters.create()
|
interp = interpreters.create()
|
||||||
raise Exception
|
try:
|
||||||
|
raise Exception
|
||||||
|
finally:
|
||||||
|
interp.close()
|
||||||
''']
|
''']
|
||||||
proc = subprocess.run(argv, capture_output=True, text=True)
|
proc = subprocess.run(argv, capture_output=True, text=True)
|
||||||
self.assertIn('Traceback', proc.stderr)
|
self.assertIn('Traceback', proc.stderr)
|
||||||
|
|
|
||||||
|
|
@ -1718,10 +1718,7 @@ def f():
|
||||||
|
|
||||||
_testcapi.run_in_subinterp(%r)
|
_testcapi.run_in_subinterp(%r)
|
||||||
""" % (subinterp_code,)
|
""" % (subinterp_code,)
|
||||||
with test.support.SuppressCrashReport():
|
assert_python_ok("-c", script)
|
||||||
rc, out, err = assert_python_failure("-c", script)
|
|
||||||
self.assertIn("Fatal Python error: Py_EndInterpreter: "
|
|
||||||
"not the last thread", err.decode())
|
|
||||||
|
|
||||||
def _check_allowed(self, before_start='', *,
|
def _check_allowed(self, before_start='', *,
|
||||||
allowed=True,
|
allowed=True,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Fix a crash when using threads inside of a subinterpreter.
|
||||||
|
|
@ -1434,9 +1434,12 @@ static int test_audit_subinterpreter(void)
|
||||||
PySys_AddAuditHook(_audit_subinterpreter_hook, NULL);
|
PySys_AddAuditHook(_audit_subinterpreter_hook, NULL);
|
||||||
_testembed_Py_InitializeFromConfig();
|
_testembed_Py_InitializeFromConfig();
|
||||||
|
|
||||||
Py_NewInterpreter();
|
PyThreadState *tstate = PyThreadState_Get();
|
||||||
Py_NewInterpreter();
|
for (int i = 0; i < 3; ++i)
|
||||||
Py_NewInterpreter();
|
{
|
||||||
|
Py_EndInterpreter(Py_NewInterpreter());
|
||||||
|
PyThreadState_Swap(tstate);
|
||||||
|
}
|
||||||
|
|
||||||
Py_Finalize();
|
Py_Finalize();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1999,6 +1999,7 @@ resolve_final_tstate(_PyRuntimeState *runtime)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
/* Fall back to the current tstate. It's better than nothing. */
|
/* Fall back to the current tstate. It's better than nothing. */
|
||||||
|
// XXX No it's not
|
||||||
main_tstate = tstate;
|
main_tstate = tstate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2044,6 +2045,16 @@ _Py_Finalize(_PyRuntimeState *runtime)
|
||||||
|
|
||||||
_PyAtExit_Call(tstate->interp);
|
_PyAtExit_Call(tstate->interp);
|
||||||
|
|
||||||
|
/* Clean up any lingering subinterpreters.
|
||||||
|
|
||||||
|
Two preconditions need to be met here:
|
||||||
|
|
||||||
|
- This has to happen before _PyRuntimeState_SetFinalizing is
|
||||||
|
called, or else threads might get prematurely blocked.
|
||||||
|
- The world must not be stopped, as finalizers can run.
|
||||||
|
*/
|
||||||
|
finalize_subinterpreters();
|
||||||
|
|
||||||
assert(_PyThreadState_GET() == tstate);
|
assert(_PyThreadState_GET() == tstate);
|
||||||
|
|
||||||
/* Copy the core config, PyInterpreterState_Delete() free
|
/* Copy the core config, PyInterpreterState_Delete() free
|
||||||
|
|
@ -2131,9 +2142,6 @@ _Py_Finalize(_PyRuntimeState *runtime)
|
||||||
_PyImport_FiniExternal(tstate->interp);
|
_PyImport_FiniExternal(tstate->interp);
|
||||||
finalize_modules(tstate);
|
finalize_modules(tstate);
|
||||||
|
|
||||||
/* Clean up any lingering subinterpreters. */
|
|
||||||
finalize_subinterpreters();
|
|
||||||
|
|
||||||
/* Print debug stats if any */
|
/* Print debug stats if any */
|
||||||
_PyEval_Fini();
|
_PyEval_Fini();
|
||||||
|
|
||||||
|
|
@ -2414,9 +2422,8 @@ Py_NewInterpreter(void)
|
||||||
return tstate;
|
return tstate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Delete an interpreter and its last thread. This requires that the
|
/* Delete an interpreter. This requires that the given thread state
|
||||||
given thread state is current, that the thread has no remaining
|
is current, and that the thread has no remaining frames.
|
||||||
frames, and that it is its interpreter's only remaining thread.
|
|
||||||
It is a fatal error to violate these constraints.
|
It is a fatal error to violate these constraints.
|
||||||
|
|
||||||
(Py_FinalizeEx() doesn't have these constraints -- it zaps
|
(Py_FinalizeEx() doesn't have these constraints -- it zaps
|
||||||
|
|
@ -2446,15 +2453,20 @@ Py_EndInterpreter(PyThreadState *tstate)
|
||||||
_Py_FinishPendingCalls(tstate);
|
_Py_FinishPendingCalls(tstate);
|
||||||
|
|
||||||
_PyAtExit_Call(tstate->interp);
|
_PyAtExit_Call(tstate->interp);
|
||||||
|
_PyRuntimeState *runtime = interp->runtime;
|
||||||
if (tstate != interp->threads.head || tstate->next != NULL) {
|
_PyEval_StopTheWorldAll(runtime);
|
||||||
Py_FatalError("not the last thread");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remaining daemon threads will automatically exit
|
/* Remaining daemon threads will automatically exit
|
||||||
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
|
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
|
||||||
_PyInterpreterState_SetFinalizing(interp, tstate);
|
_PyInterpreterState_SetFinalizing(interp, tstate);
|
||||||
|
|
||||||
|
PyThreadState *list = _PyThreadState_RemoveExcept(tstate);
|
||||||
|
for (PyThreadState *p = list; p != NULL; p = p->next) {
|
||||||
|
_PyThreadState_SetShuttingDown(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
_PyEval_StartTheWorldAll(runtime);
|
||||||
|
_PyThreadState_DeleteList(list, /*is_after_fork=*/0);
|
||||||
|
|
||||||
// XXX Call something like _PyImport_Disable() here?
|
// XXX Call something like _PyImport_Disable() here?
|
||||||
|
|
||||||
_PyImport_FiniExternal(tstate->interp);
|
_PyImport_FiniExternal(tstate->interp);
|
||||||
|
|
@ -2484,6 +2496,8 @@ finalize_subinterpreters(void)
|
||||||
PyInterpreterState *main_interp = _PyInterpreterState_Main();
|
PyInterpreterState *main_interp = _PyInterpreterState_Main();
|
||||||
assert(final_tstate->interp == main_interp);
|
assert(final_tstate->interp == main_interp);
|
||||||
_PyRuntimeState *runtime = main_interp->runtime;
|
_PyRuntimeState *runtime = main_interp->runtime;
|
||||||
|
assert(!runtime->stoptheworld.world_stopped);
|
||||||
|
assert(_PyRuntimeState_GetFinalizing(runtime) == NULL);
|
||||||
struct pyinterpreters *interpreters = &runtime->interpreters;
|
struct pyinterpreters *interpreters = &runtime->interpreters;
|
||||||
|
|
||||||
/* Get the first interpreter in the list. */
|
/* Get the first interpreter in the list. */
|
||||||
|
|
@ -2512,27 +2526,17 @@ finalize_subinterpreters(void)
|
||||||
|
|
||||||
/* Clean up all remaining subinterpreters. */
|
/* Clean up all remaining subinterpreters. */
|
||||||
while (interp != NULL) {
|
while (interp != NULL) {
|
||||||
assert(!_PyInterpreterState_IsRunningMain(interp));
|
/* Make a tstate for finalization. */
|
||||||
|
PyThreadState *tstate = _PyThreadState_NewBound(interp, _PyThreadState_WHENCE_FINI);
|
||||||
/* Find the tstate to use for fini. We assume the interpreter
|
if (tstate == NULL) {
|
||||||
will have at most one tstate at this point. */
|
// XXX Some graceful way to always get a thread state?
|
||||||
PyThreadState *tstate = interp->threads.head;
|
Py_FatalError("thread state allocation failed");
|
||||||
if (tstate != NULL) {
|
|
||||||
/* Ideally we would be able to use tstate as-is, and rely
|
|
||||||
on it being in a ready state: no exception set, not
|
|
||||||
running anything (tstate->current_frame), matching the
|
|
||||||
current thread ID (tstate->thread_id). To play it safe,
|
|
||||||
we always delete it and use a fresh tstate instead. */
|
|
||||||
assert(tstate != final_tstate);
|
|
||||||
_PyThreadState_Attach(tstate);
|
|
||||||
PyThreadState_Clear(tstate);
|
|
||||||
_PyThreadState_Detach(tstate);
|
|
||||||
PyThreadState_Delete(tstate);
|
|
||||||
}
|
}
|
||||||
tstate = _PyThreadState_NewBound(interp, _PyThreadState_WHENCE_FINI);
|
|
||||||
|
/* Enter the subinterpreter. */
|
||||||
|
_PyThreadState_Attach(tstate);
|
||||||
|
|
||||||
/* Destroy the subinterpreter. */
|
/* Destroy the subinterpreter. */
|
||||||
_PyThreadState_Attach(tstate);
|
|
||||||
Py_EndInterpreter(tstate);
|
Py_EndInterpreter(tstate);
|
||||||
assert(_PyThreadState_GET() == NULL);
|
assert(_PyThreadState_GET() == NULL);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue