History
There’s been a desire to have an API to safely acquire the GIL (or really, an attached thread state) when Python could finalize (or is finalizing), which isn’t possible with PyGILState_Ensure
, or any public API right now. The problem is that even with a check like Py_IsFinalizing
, the runtime could still finalize during the call to PyGILState_Ensure
, which will (most likely) end up crashing. There’s an open issue about this too: Fail PyGILState_Ensure() If Finalizing? · Issue #124622 · python/cpython · GitHub
Originally, the plan was to call this new function PyGILState_EnsureOrFail
, but I had some concerns about the name. Mainly, the term “GIL” isn’t something we want in the public API anymore; there can be more than one GIL (so “the GIL” isn’t something that really exists), and the GIL can be disabled entirely on free-threaded builds. I’ve outlined this more clearly in an unrelated issue.
Additionally, a few months ago, @eric.snow, @encukou, and I discussed some of the implications that PyGILState_Ensure
has on subinterpreters. If a thread spawned by a subinterpreter were to call PyGILState_Ensure
, then the thread will have the GIL of the main interpreter, rather than the subinterpreter. That means the thread can’t safely interact with the calling interpreter at all.
So, it’s pretty clear that we need a new API that covers a few bases:
- Returns errors that can be handled–
PyGILState_Ensure
either segfaults, hangs the thread, or emits a fatal error. - Can safely run at finalization without crashing.
- Takes an explicit interpreter to prevent problems with interpreter mismatch.
- Semantically clear that we’re dealing with thread states, not necessarily the GIL.
Proposal
I’m proposing two new functions:
int PyThreadState_Ensure(PyInterpreterState *interp, const char **errmsg);
void PyThreadState_Release();
The usage would be like this:
/* Dummy structure to hold a Python object and interpreter */
typedef struct {
PyObject *callable;
PyInterpreterState *interp;
} pyrun_t;
static int
c_thread_func(pyrun_t *to_run)
{
/* Acquire a thread state so we can call Python */
const char *err;
if (PyThreadState_Ensure(to_run->interp, &err) < 0) {
fprintf(stderr, "Failed to talk to Python: %s\n", err);
abort();
}
/* Run the function */
/* ... */
PyThreadState_Release();
}
@vstinner has been very helpful in implementing this so far:
But, there’s still some parts of the API that need to get ironed out, so that’s why I’m here.
- Currently, it returns
-1
on failure, and is passed a pointer to a C string to set the error message. I’m fine with this, but users cannot safely rely on the error message to differentiate between errors–if that’s something people want to do, it might be worth supplying an error code alongside the message. So, something like this instead of aconst char **
:
typedef enum {
OUT_OF_MEMORY,
INTERPRETER_FINALIZING,
/* ... */
} PyThreadState_ErrorCode;
typedef struct {
const char *message;
PyThreadState_ErrorCode code;
} PyThreadState_Error;
- Is there any added benefit to passing an
int
handle around, instead of internally storing that on the thread state itself? I’m not a fan of this myself, but there’s no consensus around it, and it’s slightly easier to implement. The API would look like this, then:
static int
c_thread_func(pyrun_t *to_run)
{
const char *err;
int state = PyThreadState_Ensure(to_run->interp, &err);
if (state < 0) {
fprintf(stderr, "Failed to talk to Python: %s\n", err);
abort();
}
PyThreadState_Release(state);
}
- This is a more technical point, but it might be worth using an interpreter ID rather than an interpreter state, because a state might get invalidated if the interpreter exits. It’s more clunky for users though, because there has to be an extra
PyInterpreterState_GetID
call.
Feel free to add any other ideas/concerns here too.
Deprecate PyGILState
?
I think it would be good to fully deprecate all PyGILState
APIs–they’re confusing, buggy, and will have a much better replacement. Even in the public API today, you can replicate everything in PyGILState
with a less ambiguous PyThreadState
API:
PyGILState_Ensure
→PyThreadState_Swap
/PyThreadState_New
(would instead bePyThreadState_Ensure
)PyGILState_Release
→PyThreadState_Clear
/PyThreadState_Delete
(would instead bePyThreadState_Release
)PyGILState_GetThisThreadState
→PyThreadState_Get
PyGILState_Check
→PyThreadState_GetUnchecked() != NULL
(in fact, we recently removed all usages ofassert(PyGILState_Check())
internally).