Skip to content
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

[3.12] gh-76785: Use Pending Calls When Releasing Cross-Interpreter Data (gh-109556) #109586

Closed
Closed
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
2 changes: 1 addition & 1 deletion Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extern void _PyEval_FiniState(struct _ceval_state *ceval);
PyAPI_FUNC(void) _PyEval_SignalReceived(PyInterpreterState *interp);
PyAPI_FUNC(int) _PyEval_AddPendingCall(
PyInterpreterState *interp,
int (*func)(void *),
_Py_pending_call_func func,
void *arg,
int mainthreadonly);
PyAPI_FUNC(void) _PyEval_SignalAsyncExc(PyInterpreterState *interp);
Expand Down
4 changes: 3 additions & 1 deletion Include/internal/pycore_ceval_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ extern "C" {
#include "pycore_gil.h" // struct _gil_runtime_state


typedef int (*_Py_pending_call_func)(void *);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this refactoring should be done in 3.12.1. Is there a reason not to leave it just 'int (*func)(void *)' in a few places?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, _Py_pending_call_func() is not necessary. I'm fine with dropping it.


struct _pending_calls {
int busy;
PyThread_type_lock lock;
Expand All @@ -24,7 +26,7 @@ struct _pending_calls {
int async_exc;
#define NPENDINGCALLS 32
struct _pending_call {
int (*func)(void *);
_Py_pending_call_func func;
void *arg;
} calls[NPENDINGCALLS];
int first;
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ extern PyStatus _PyInterpreterState_DeleteExceptMain(_PyRuntimeState *runtime);
extern void _PySignal_AfterFork(void);
#endif

PyAPI_FUNC(int) _PyCrossInterpreterData_ReleaseAndRawFree(_PyCrossInterpreterData *);


PyAPI_FUNC(int) _PyState_AddModule(
PyThreadState *tstate,
Expand Down
35 changes: 25 additions & 10 deletions Modules/_xxinterpchannelsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
/* interpreters module */
/* low-level access to interpreter primitives */

#ifndef Py_BUILD_CORE_BUILTIN
# define Py_BUILD_CORE_MODULE 1
#endif

#include "Python.h"
#include "interpreteridobject.h"
#include "pycore_pystate.h" // _PyCrossInterpreterData_ReleaseAndRawFree()


/*
Expand Down Expand Up @@ -161,21 +166,34 @@ add_new_type(PyObject *mod, PyType_Spec *spec, crossinterpdatafunc shared)
return cls;
}

#define XID_IGNORE_EXC 1
#define XID_FREE 2

static int
_release_xid_data(_PyCrossInterpreterData *data, int ignoreexc)
_release_xid_data(_PyCrossInterpreterData *data, int flags)
{
int ignoreexc = flags & XID_IGNORE_EXC;
PyObject *exc;
if (ignoreexc) {
exc = PyErr_GetRaisedException();
}
int res = _PyCrossInterpreterData_Release(data);
int res;
if (flags & XID_FREE) {
res = _PyCrossInterpreterData_ReleaseAndRawFree(data);
}
else {
res = _PyCrossInterpreterData_Release(data);
}
if (res < 0) {
/* The owning interpreter is already destroyed. */
if (ignoreexc) {
// XXX Emit a warning?
PyErr_Clear();
}
}
if (flags & XID_FREE) {
/* Either way, we free the data. */
}
if (ignoreexc) {
PyErr_SetRaisedException(exc);
}
Expand Down Expand Up @@ -367,9 +385,8 @@ static void
_channelitem_clear(_channelitem *item)
{
if (item->data != NULL) {
(void)_release_xid_data(item->data, 1);
// It was allocated in _channel_send().
GLOBAL_FREE(item->data);
(void)_release_xid_data(item->data, XID_IGNORE_EXC & XID_FREE);
item->data = NULL;
}
item->next = NULL;
Expand Down Expand Up @@ -1440,14 +1457,12 @@ _channel_recv(_channels *channels, int64_t id, PyObject **res)
PyObject *obj = _PyCrossInterpreterData_NewObject(data);
if (obj == NULL) {
assert(PyErr_Occurred());
(void)_release_xid_data(data, 1);
// It was allocated in _channel_send().
GLOBAL_FREE(data);
// It was allocated in _channel_send(), so we free it.
(void)_release_xid_data(data, XID_IGNORE_EXC | XID_FREE);
return -1;
}
int release_res = _release_xid_data(data, 0);
// It was allocated in _channel_send().
GLOBAL_FREE(data);
// It was allocated in _channel_send(), so we free it.
int release_res = _release_xid_data(data, XID_FREE);
if (release_res < 0) {
// The source interpreter has been destroyed already.
assert(PyErr_Occurred());
Expand Down
29 changes: 11 additions & 18 deletions Modules/_xxsubinterpretersmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,17 @@ add_new_exception(PyObject *mod, const char *name, PyObject *base)
add_new_exception(MOD, MODULE_NAME "." Py_STRINGIFY(NAME), BASE)

static int
_release_xid_data(_PyCrossInterpreterData *data, int ignoreexc)
_release_xid_data(_PyCrossInterpreterData *data)
{
PyObject *exc;
if (ignoreexc) {
exc = PyErr_GetRaisedException();
}
PyObject *exc = PyErr_GetRaisedException();
int res = _PyCrossInterpreterData_Release(data);
if (res < 0) {
/* The owning interpreter is already destroyed. */
_PyCrossInterpreterData_Clear(NULL, data);
if (ignoreexc) {
// XXX Emit a warning?
PyErr_Clear();
}
}
if (ignoreexc) {
PyErr_SetRaisedException(exc);
// XXX Emit a warning?
PyErr_Clear();
}
PyErr_SetRaisedException(exc);
return res;
}

Expand Down Expand Up @@ -140,7 +133,7 @@ _sharednsitem_clear(struct _sharednsitem *item)
PyMem_RawFree((void *)item->name);
item->name = NULL;
}
(void)_release_xid_data(&item->data, 1);
(void)_release_xid_data(&item->data);
}

static int
Expand Down Expand Up @@ -169,16 +162,16 @@ typedef struct _sharedns {
static _sharedns *
_sharedns_new(Py_ssize_t len)
{
_sharedns *shared = PyMem_NEW(_sharedns, 1);
_sharedns *shared = PyMem_RawCalloc(sizeof(_sharedns), 1);
if (shared == NULL) {
PyErr_NoMemory();
return NULL;
}
shared->len = len;
shared->items = PyMem_NEW(struct _sharednsitem, len);
shared->items = PyMem_RawCalloc(sizeof(struct _sharednsitem), len);
if (shared->items == NULL) {
PyErr_NoMemory();
PyMem_Free(shared);
PyMem_RawFree(shared);
return NULL;
}
return shared;
Expand All @@ -190,8 +183,8 @@ _sharedns_free(_sharedns *shared)
for (Py_ssize_t i=0; i < shared->len; i++) {
_sharednsitem_clear(&shared->items[i]);
}
PyMem_Free(shared->items);
PyMem_Free(shared);
PyMem_RawFree(shared->items);
PyMem_RawFree(shared);
}

static _sharedns *
Expand Down
8 changes: 4 additions & 4 deletions Python/ceval_gil.c
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ _PyEval_SignalReceived(PyInterpreterState *interp)
/* Push one item onto the queue while holding the lock. */
static int
_push_pending_call(struct _pending_calls *pending,
int (*func)(void *), void *arg)
_Py_pending_call_func func, void *arg)
{
int i = pending->last;
int j = (i + 1) % NPENDINGCALLS;
Expand Down Expand Up @@ -836,7 +836,7 @@ _pop_pending_call(struct _pending_calls *pending,

int
_PyEval_AddPendingCall(PyInterpreterState *interp,
int (*func)(void *), void *arg,
_Py_pending_call_func func, void *arg,
int mainthreadonly)
{
assert(!mainthreadonly || _Py_IsMainInterpreter(interp));
Expand All @@ -860,7 +860,7 @@ _PyEval_AddPendingCall(PyInterpreterState *interp,
}

int
Py_AddPendingCall(int (*func)(void *), void *arg)
Py_AddPendingCall(_Py_pending_call_func func, void *arg)
{
/* Legacy users of this API will continue to target the main thread
(of the main interpreter). */
Expand Down Expand Up @@ -904,7 +904,7 @@ _make_pending_calls(struct _pending_calls *pending)
{
/* perform a bounded number of calls, in case of recursion */
for (int i=0; i<NPENDINGCALLS; i++) {
int (*func)(void *) = NULL;
_Py_pending_call_func func = NULL;
void *arg = NULL;

/* pop one item off the queue while holding the lock */
Expand Down
91 changes: 58 additions & 33 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -2255,10 +2255,16 @@ _xidata_init(_PyCrossInterpreterData *data)
static inline void
_xidata_clear(_PyCrossInterpreterData *data)
{
if (data->free != NULL) {
data->free(data->data);
// _PyCrossInterpreterData only has two members that need to be
// cleaned up, if set: "data" must be freed and "obj" must be decref'ed.
// In both cases the original (owning) interpreter must be used,
// which is the caller's responsibility to ensure.
if (data->data != NULL) {
if (data->free != NULL) {
data->free(data->data);
}
data->data = NULL;
}
data->data = NULL;
Py_CLEAR(data->obj);
}

Expand Down Expand Up @@ -2403,40 +2409,32 @@ _PyCrossInterpreterData_NewObject(_PyCrossInterpreterData *data)
return data->new_object(data);
}

typedef void (*releasefunc)(PyInterpreterState *, void *);

static void
_call_in_interpreter(PyInterpreterState *interp, releasefunc func, void *arg)
static int
_release_xidata_pending(void *data)
{
/* We would use Py_AddPendingCall() if it weren't specific to the
* main interpreter (see bpo-33608). In the meantime we take a
* naive approach.
*/
_PyRuntimeState *runtime = interp->runtime;
PyThreadState *save_tstate = NULL;
if (interp != current_fast_get(runtime)->interp) {
// XXX Using the "head" thread isn't strictly correct.
PyThreadState *tstate = PyInterpreterState_ThreadHead(interp);
// XXX Possible GILState issues?
save_tstate = _PyThreadState_Swap(runtime, tstate);
}

// XXX Once the GIL is per-interpreter, this should be called with the
// calling interpreter's GIL released and the target interpreter's held.
func(interp, arg);
_xidata_clear((_PyCrossInterpreterData *)data);
return 0;
}

// Switch back.
if (save_tstate != NULL) {
_PyThreadState_Swap(runtime, save_tstate);
}
static int
_xidata_release_and_rawfree_pending(void *data)
{
_xidata_clear((_PyCrossInterpreterData *)data);
PyMem_RawFree(data);
return 0;
}

int
_PyCrossInterpreterData_Release(_PyCrossInterpreterData *data)
static int
_xidata_release(_PyCrossInterpreterData *data, int rawfree)
{
if (data->free == NULL && data->obj == NULL) {
if ((data->data == NULL || data->free == NULL) && data->obj == NULL) {
// Nothing to release!
data->data = NULL;
if (rawfree) {
PyMem_RawFree(data);
}
else {
data->data = NULL;
}
return 0;
}

Expand All @@ -2447,15 +2445,42 @@ _PyCrossInterpreterData_Release(_PyCrossInterpreterData *data)
// This function shouldn't have been called.
// XXX Someone leaked some memory...
assert(PyErr_Occurred());
if (rawfree) {
PyMem_RawFree(data);
}
return -1;
}

// "Release" the data and/or the object.
_call_in_interpreter(interp,
(releasefunc)_PyCrossInterpreterData_Clear, data);
if (interp == current_fast_get(interp->runtime)->interp) {
_xidata_clear(data);
if (rawfree) {
PyMem_RawFree(data);
}
}
else {
_Py_pending_call_func func = _release_xidata_pending;
if (rawfree) {
func = _xidata_release_and_rawfree_pending;
}
// XXX Emit a warning if this fails?
_PyEval_AddPendingCall(interp, func, data, 0);
}
return 0;
}

int
_PyCrossInterpreterData_Release(_PyCrossInterpreterData *data)
{
return _xidata_release(data, 0);
}

int
_PyCrossInterpreterData_ReleaseAndRawFree(_PyCrossInterpreterData *data)
{
return _xidata_release(data, 1);
}

/* registry of {type -> crossinterpdatafunc} */

/* For now we use a global registry of shareable classes. An
Expand Down