gh-136003: Execute pre-finalization callbacks in a loop (GH-136004)

This commit is contained in:
Peter Bierma 2025-09-18 08:29:12 -04:00 committed by GitHub
parent d6a6fe2a5b
commit 2191497933
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 281 additions and 43 deletions

View file

@ -2013,6 +2013,133 @@ resolve_final_tstate(_PyRuntimeState *runtime)
return main_tstate;
}
#ifdef Py_GIL_DISABLED
#define ASSERT_WORLD_STOPPED(interp) assert(interp->runtime->stoptheworld.world_stopped)
#else
#define ASSERT_WORLD_STOPPED(interp)
#endif
static int
interp_has_threads(PyInterpreterState *interp)
{
/* This needs to check for non-daemon threads only, otherwise we get stuck
* in an infinite loop. */
assert(interp != NULL);
ASSERT_WORLD_STOPPED(interp);
assert(interp->threads.head != NULL);
if (interp->threads.head->next == NULL) {
// No other threads active, easy way out.
return 0;
}
// We don't have to worry about locking this because the
// world is stopped.
_Py_FOR_EACH_TSTATE_UNLOCKED(interp, tstate) {
if (tstate->_whence == _PyThreadState_WHENCE_THREADING) {
return 1;
}
}
return 0;
}
static int
interp_has_pending_calls(PyInterpreterState *interp)
{
assert(interp != NULL);
ASSERT_WORLD_STOPPED(interp);
return interp->ceval.pending.npending != 0;
}
static int
interp_has_atexit_callbacks(PyInterpreterState *interp)
{
assert(interp != NULL);
assert(interp->atexit.callbacks != NULL);
ASSERT_WORLD_STOPPED(interp);
assert(PyList_CheckExact(interp->atexit.callbacks));
return PyList_GET_SIZE(interp->atexit.callbacks) != 0;
}
static int
runtime_has_subinterpreters(_PyRuntimeState *runtime)
{
assert(runtime != NULL);
HEAD_LOCK(runtime);
PyInterpreterState *interp = runtime->interpreters.head;
HEAD_UNLOCK(runtime);
return interp->next != NULL;
}
static void
make_pre_finalization_calls(PyThreadState *tstate, int subinterpreters)
{
assert(tstate != NULL);
PyInterpreterState *interp = tstate->interp;
/* Each of these functions can start one another, e.g. a pending call
* could start a thread or vice versa. To ensure that we properly clean
* call everything, we run these in a loop until none of them run anything. */
for (;;) {
assert(!interp->runtime->stoptheworld.world_stopped);
// Wrap up existing "threading"-module-created, non-daemon threads.
wait_for_thread_shutdown(tstate);
// Make any remaining pending calls.
_Py_FinishPendingCalls(tstate);
/* The interpreter is still entirely intact at this point, and the
* exit funcs may be relying on that. In particular, if some thread
* or exit func is still waiting to do an import, the import machinery
* expects Py_IsInitialized() to return true. So don't say the
* runtime is uninitialized until after the exit funcs have run.
* Note that Threading.py uses an exit func to do a join on all the
* threads created thru it, so this also protects pending imports in
* the threads created via Threading.
*/
_PyAtExit_Call(tstate->interp);
if (subinterpreters) {
/* 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();
}
/* Stop the world to prevent other threads from creating threads or
* atexit callbacks. On the default build, this is simply locked by
* the GIL. For pending calls, we acquire the dedicated mutex, because
* Py_AddPendingCall() can be called without an attached thread state.
*/
PyMutex_Lock(&interp->ceval.pending.mutex);
// XXX Why does _PyThreadState_DeleteList() rely on all interpreters
// being stopped?
_PyEval_StopTheWorldAll(interp->runtime);
int has_subinterpreters = subinterpreters
? runtime_has_subinterpreters(interp->runtime)
: 0;
int should_continue = (interp_has_threads(interp)
|| interp_has_atexit_callbacks(interp)
|| interp_has_pending_calls(interp)
|| has_subinterpreters);
if (!should_continue) {
break;
}
_PyEval_StartTheWorldAll(interp->runtime);
PyMutex_Unlock(&interp->ceval.pending.mutex);
}
assert(PyMutex_IsLocked(&interp->ceval.pending.mutex));
ASSERT_WORLD_STOPPED(interp);
}
static int
_Py_Finalize(_PyRuntimeState *runtime)
{
@ -2029,33 +2156,8 @@ _Py_Finalize(_PyRuntimeState *runtime)
// Block some operations.
tstate->interp->finalizing = 1;
// Wrap up existing "threading"-module-created, non-daemon threads.
wait_for_thread_shutdown(tstate);
// Make any remaining pending calls.
_Py_FinishPendingCalls(tstate);
/* The interpreter is still entirely intact at this point, and the
* exit funcs may be relying on that. In particular, if some thread
* or exit func is still waiting to do an import, the import machinery
* expects Py_IsInitialized() to return true. So don't say the
* runtime is uninitialized until after the exit funcs have run.
* Note that Threading.py uses an exit func to do a join on all the
* threads created thru it, so this also protects pending imports in
* the threads created via Threading.
*/
_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();
// This call stops the world and takes the pending calls lock.
make_pre_finalization_calls(tstate, /*subinterpreters=*/1);
assert(_PyThreadState_GET() == tstate);
@ -2073,7 +2175,7 @@ _Py_Finalize(_PyRuntimeState *runtime)
#endif
/* Ensure that remaining threads are detached */
_PyEval_StopTheWorldAll(runtime);
ASSERT_WORLD_STOPPED(tstate->interp);
/* Remaining daemon threads will be trapped in PyThread_hang_thread
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
@ -2094,6 +2196,7 @@ _Py_Finalize(_PyRuntimeState *runtime)
_PyThreadState_SetShuttingDown(p);
}
_PyEval_StartTheWorldAll(runtime);
PyMutex_Unlock(&tstate->interp->ceval.pending.mutex);
/* Clear frames of other threads to call objects destructors. Destructors
will be called in the current Python thread. Since
@ -2449,15 +2552,10 @@ Py_EndInterpreter(PyThreadState *tstate)
}
interp->finalizing = 1;
// Wrap up existing "threading"-module-created, non-daemon threads.
wait_for_thread_shutdown(tstate);
// This call stops the world and takes the pending calls lock.
make_pre_finalization_calls(tstate, /*subinterpreters=*/0);
// Make any remaining pending calls.
_Py_FinishPendingCalls(tstate);
_PyAtExit_Call(tstate->interp);
_PyRuntimeState *runtime = interp->runtime;
_PyEval_StopTheWorldAll(runtime);
ASSERT_WORLD_STOPPED(interp);
/* Remaining daemon threads will automatically exit
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
_PyInterpreterState_SetFinalizing(interp, tstate);
@ -2467,7 +2565,8 @@ Py_EndInterpreter(PyThreadState *tstate)
_PyThreadState_SetShuttingDown(p);
}
_PyEval_StartTheWorldAll(runtime);
_PyEval_StartTheWorldAll(interp->runtime);
PyMutex_Unlock(&interp->ceval.pending.mutex);
_PyThreadState_DeleteList(list, /*is_after_fork=*/0);
// XXX Call something like _PyImport_Disable() here?

View file

@ -1461,7 +1461,7 @@ init_threadstate(_PyThreadStateImpl *_tstate,
assert(tstate->prev == NULL);
assert(tstate->_whence == _PyThreadState_WHENCE_NOTSET);
assert(whence >= 0 && whence <= _PyThreadState_WHENCE_EXEC);
assert(whence >= 0 && whence <= _PyThreadState_WHENCE_THREADING_DAEMON);
tstate->_whence = whence;
assert(id > 0);