Skip to content

bpo-33608: Deal with pending calls relative to runtime shutdown. #12246

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

Merged
Merged
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
3 changes: 3 additions & 0 deletions Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ extern "C" {
#include "pycore_atomic.h"
#include "pythread.h"

PyAPI_FUNC(void) _Py_FinishPendingCalls(void);

struct _pending_calls {
int finishing;
PyThread_type_lock lock;
/* Request for running pending calls. */
_Py_atomic_int calls_to_do;
Expand Down
82 changes: 61 additions & 21 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -330,31 +330,33 @@ _PyEval_SignalReceived(void)

/* Push one item onto the queue while holding the lock. */
static int
_push_pending_call(int (*func)(void *), void *arg)
_push_pending_call(struct _pending_calls *pending,
int (*func)(void *), void *arg)
{
int i = _PyRuntime.ceval.pending.last;
int i = pending->last;
int j = (i + 1) % NPENDINGCALLS;
if (j == _PyRuntime.ceval.pending.first) {
if (j == pending->first) {
return -1; /* Queue full */
}
_PyRuntime.ceval.pending.calls[i].func = func;
_PyRuntime.ceval.pending.calls[i].arg = arg;
_PyRuntime.ceval.pending.last = j;
pending->calls[i].func = func;
pending->calls[i].arg = arg;
pending->last = j;
return 0;
}

/* Pop one item off the queue while holding the lock. */
static void
_pop_pending_call(int (**func)(void *), void **arg)
_pop_pending_call(struct _pending_calls *pending,
int (**func)(void *), void **arg)
{
int i = _PyRuntime.ceval.pending.first;
if (i == _PyRuntime.ceval.pending.last) {
int i = pending->first;
if (i == pending->last) {
return; /* Queue empty */
}

*func = _PyRuntime.ceval.pending.calls[i].func;
*arg = _PyRuntime.ceval.pending.calls[i].arg;
_PyRuntime.ceval.pending.first = (i + 1) % NPENDINGCALLS;
*func = pending->calls[i].func;
*arg = pending->calls[i].arg;
pending->first = (i + 1) % NPENDINGCALLS;
}

/* This implementation is thread-safe. It allows
Expand All @@ -365,9 +367,23 @@ _pop_pending_call(int (**func)(void *), void **arg)
int
Py_AddPendingCall(int (*func)(void *), void *arg)
{
PyThread_acquire_lock(_PyRuntime.ceval.pending.lock, WAIT_LOCK);
int result = _push_pending_call(func, arg);
PyThread_release_lock(_PyRuntime.ceval.pending.lock);
struct _pending_calls *pending = &_PyRuntime.ceval.pending;

PyThread_acquire_lock(pending->lock, WAIT_LOCK);
if (pending->finishing) {
PyThread_release_lock(pending->lock);

PyObject *exc, *val, *tb;
PyErr_Fetch(&exc, &val, &tb);
PyErr_SetString(PyExc_SystemError,
"Py_AddPendingCall: cannot add pending calls "
"(Python shutting down)");
PyErr_Print();
PyErr_Restore(exc, val, tb);
return -1;
}
int result = _push_pending_call(pending, func, arg);
PyThread_release_lock(pending->lock);

/* signal main loop */
SIGNAL_PENDING_CALLS();
Expand Down Expand Up @@ -400,7 +416,7 @@ handle_signals(void)
}

static int
make_pending_calls(void)
make_pending_calls(struct _pending_calls* pending)
{
static int busy = 0;

Expand All @@ -425,9 +441,9 @@ make_pending_calls(void)
void *arg = NULL;

/* pop one item off the queue while holding the lock */
PyThread_acquire_lock(_PyRuntime.ceval.pending.lock, WAIT_LOCK);
_pop_pending_call(&func, &arg);
PyThread_release_lock(_PyRuntime.ceval.pending.lock);
PyThread_acquire_lock(pending->lock, WAIT_LOCK);
_pop_pending_call(pending, &func, &arg);
PyThread_release_lock(pending->lock);

/* having released the lock, perform the callback */
if (func == NULL) {
Expand All @@ -448,6 +464,30 @@ make_pending_calls(void)
return res;
}

void
_Py_FinishPendingCalls(void)
{
struct _pending_calls *pending = &_PyRuntime.ceval.pending;

assert(PyGILState_Check());

PyThread_acquire_lock(pending->lock, WAIT_LOCK);
pending->finishing = 1;
PyThread_release_lock(pending->lock);

if (!_Py_atomic_load_relaxed(&(pending->calls_to_do))) {
return;
}

if (make_pending_calls(pending) < 0) {
PyObject *exc, *val, *tb;
PyErr_Fetch(&exc, &val, &tb);
PyErr_BadInternalCall();
_PyErr_ChainExceptions(exc, val, tb);
PyErr_Print();
}
}

/* Py_MakePendingCalls() is a simple wrapper for the sake
of backward-compatibility. */
int
Expand All @@ -462,7 +502,7 @@ Py_MakePendingCalls(void)
return res;
}

res = make_pending_calls();
res = make_pending_calls(&_PyRuntime.ceval.pending);
if (res != 0) {
return res;
}
Expand Down Expand Up @@ -1012,7 +1052,7 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
if (_Py_atomic_load_relaxed(
&_PyRuntime.ceval.pending.calls_to_do))
{
if (make_pending_calls() != 0) {
if (make_pending_calls(&_PyRuntime.ceval.pending) != 0) {
goto error;
}
}
Expand Down
7 changes: 6 additions & 1 deletion Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -1049,17 +1049,21 @@ Py_FinalizeEx(void)
if (!_PyRuntime.initialized)
return status;

// Wrap up existing "threading"-module-created, non-daemon threads.
wait_for_thread_shutdown();

/* Get current thread state and interpreter pointer */
tstate = _PyThreadState_GET();
interp = tstate->interp;

// Make any remaining pending calls.
_Py_FinishPendingCalls();

/* 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
* interpreter is uninitialized until after the exit funcs have run.
* 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.
Expand Down Expand Up @@ -1462,6 +1466,7 @@ Py_EndInterpreter(PyThreadState *tstate)
Py_FatalError("Py_EndInterpreter: thread still has a frame");
interp->finalizing = 1;

// Wrap up existing "threading"-module-created, non-daemon threads.
wait_for_thread_shutdown();

call_py_exitfuncs(interp);
Expand Down