GeezerVison is showing github.com — Subinterpreters don't properly clean up threads · Issue #128639 · python/cpython
Subinterpreters don't properly clean up threads #128639DescriptionCrash report
What happened?
I found this issue a little while back, but I'm finally getting around to fixing it.
Currently, subinterpreter finalization assumes that there's only one thread left. So, any remaining threads--daemon or non-daemon--crash the interpreter upon finalizing:
import _interpreters
interp = _interpreters.create()
source = """
import threading
import time
def hello():
time.sleep(1)
t = threading.Thread(target=hello)
t.start()
"""
_interpreters.run_string(interp, source)
This results in an assertion failure on my end:
python: Python/pystate.c:1969: tstate_activate: Assertion `!tstate->_status.bound_gilstate || tstate == gilstate_tss_get((tstate->interp->runtime))' failed.
So, there's a few things that need to get fixed:
- The finalization process shouldn't assume that there's one thread. Instead, it should just dive straight into
Py_EndInterpreter, which will properly clean things up.
- It also shouldn't try to manually delete threads via attaching to them and then calling
PyThreadState_Delete. That should also happen in Py_EndInterpreter, after all threads have finished.
- Subinterpreter finalization itself happens way too late.
_PyRuntimeState_SetFinalizing has been set, meaning that all threads will be already blocked, and thus cannot shutdown via threading._shutdown, resulting in a deadlock. finalize_subinterpreters should be called before that happens.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
No response
Linked PRs
Activityericsnowcurrently commented on May 19, 2025 FWIW, I thought I had created an issue about cleaning up lingering thread states automatically. Would you mind double-checking and closing it, if so? ZeroIntensity commented on May 21, 2025 For those who aren't me or Eric: we had to revert the fix because it breaks the test suite when run with --single-process. I'm still trying to figure out why. ZeroIntensity commented on May 24, 2025 @ericsnowcurrently I finally figured out the buildbot failures. This is an awesome bug.
Here's a repro that crashes with my patch applied:
import _interpreters
import types
import time
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(1)
threads = [threading.Thread(target=task, daemon=True) for _ in range(3)]
for t in threads:
t.start()
""")
_interpreters.destroy(interp)
time.sleep(2)
The problem is that something subtly changed with daemon thread shutdown in-between writing my patch and merging it. So, after the time.sleep() call, the thread would still be alive and crash. Unfortunately for me, I randomly chose 100 seconds as the sleep time, so if the process exited within those 100 seconds, nothing would crash. I'll put up the fixed version. sergey-miryanov commented on Jul 8, 2025 I have a following repro:
from concurrent import interpreters
import _interpreters
def func(*args, **kwargs):
# pass # crash on main, ok on #134606
assert False # crash on both
def clean_up_interpreters():
for interp in interpreters.list_all():
if interp.id == 0: # main
continue
try:
interp.close()
except _interpreters.InterpreterError:
pass # already destroyed
def test():
interp = interpreters.create()
interp.call_in_thread(func)
try:
for _ in range(100):
test()
finally:
clean_up_interpreters()
I tested it on main and on #134606 (with merged main). func with assert False crashes with following stack trace:
python315.dll!PyDict_GetItemWithError(_object * op, _object * key) Line 2450 C
[Inline Frame] python315.dll!get_type_attr_as_size(_typeobject *) Line 30 C
[Inline Frame] python315.dll!PyStructSequence_New(_typeobject *) Line 64 C
python315.dll!make_unraisable_hook_args(_ts * tstate, _object * exc_type, _object * exc_value, _object * exc_tb, _object * err_msg, _object * obj) Line 1412 C
python315.dll!format_unraisable_v(const char * format, char * va, _object * obj) Line 1670 C
python315.dll!PyErr_FormatUnraisable(const char * format, ...) Line 1746 C
> python315.dll!Py_EndInterpreter(_ts * tstate) Line 2449 C
python315.dll!_PyXI_EndInterpreter(_is * interp, _ts * tstate, _ts * * p_save_tstate) Line 3285 C
python315.dll!interp_destroy(_object * self, _object * args, _object * kwds) Line 946 C
python315.dll!cfunction_call(_object * func, _object * args, _object * kwargs) Line 567 C
ZeroIntensity commented on Jul 8, 2025 No idea yet. Let's wait until we get thread cleanup fixed and then we can investigate that one in a new issue. added a commit that references this issue on Sep 17, 2025gh-128639: Don't assume one thread in subinterpreter finalization wit… sergey-miryanov commented on Nov 4, 2025 FTR, repro from #128639 (comment) seems to have been fixed. It produces:
Exception in thread Thread-100 (_call):
Traceback (most recent call last):
File "X:\Work\cpython\main\Lib\threading.py", line 1075, in _bootstrap_inner
self._context.run(self.run)
~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "X:\Work\cpython\main\Lib\threading.py", line 1017, in run
self._target(*self._args, **self._kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "X:\Work\cpython\main\Lib\concurrent\interpreters\__init__.py", line 222, in _call
raise ExecutionFailed(excinfo)
concurrent.interpreters.ExecutionFailed: AssertionError:
Uncaught in the interpreter:
Traceback (most recent call last):
File "X:\Work\cpython\issues\test.py", line 6, in func
assert False # crash on both
^^^^^
AssertionError
Thanks!
MetadataMetadataStatusShow more project fields
DevelopmentNo branches or pull requestsIssue actions
|