-
-
Notifications
You must be signed in to change notification settings - Fork 31.7k
gh-124622: Add PyThreadState_Ensure() function #130012
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
Conversation
Add PyThreadState_Ensure() and PyThreadState_Release() functions. Add new "OrFail" internal functions: * _PyEval_AcquireLockOrFail() * _PyEval_RestoreThreadOrFail() * _PyThreadState_AttachOrFail() * take_gil_or_fail()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a lot happier with this 😄! When this was first discussed a few months ago, Petr suggested using PyStatus
as the return value to differentiate between failure reasons. So, something like this:
static int
my_thread(PyInterpreterState *interp)
{
int state;
PyStatus status = PyThreadState_Ensure(interp, &state);
if (PyStatus_Exception(status)) {
// Log the error somehow
/* ... */
}
PyThreadState_Release(state);
}
I think that could be handy, especially for applications that want to continue running instead of just bailing out of the thread.
Python/pystate.c
Outdated
PyGILState_STATE | ||
PyGILState_Ensure(void) | ||
{ | ||
PyInterpreterState *interp = _PyRuntime.gilstate.autoInterpreterState; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's NULL
, we should probably just fatal error or hang the thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could add an assert
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't help much. Release builds will crash at finalization with spurious NULL
derefs if we don't add a case for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If possible, I would prefer to not change PyGILState_Ensure() in this PR which ("only") adds PyThreadState_Ensure().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be happy to do that in a follow-up then.
Python/pystate.c
Outdated
@@ -2841,6 +2865,22 @@ PyGILState_Release(PyGILState_STATE oldstate) | |||
} | |||
|
|||
|
|||
void | |||
PyThreadState_Release(int oldstate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking out loud here: instead of manually passing an int
around indicating the number of PyThreadState_Ensure
calls, why not just store that number on the thread state itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The oldstate is either PyGILState_UNLOCKED
or PyGILState_LOCKED
, it's not a counter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, hm. Why do we need PyGILState
here at all? The thread state should pretty much manage itself, there just needs to be a call to PyThreadState_Clear
and PyThreadState_DeleteCurrent
at the end (which is why I thought it would be done with a counter of calls).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that PyThreadState_Release() does basically nothing if oldstate is PyGILState_LOCKED: if the thread already hold the GIL when PyThreadState_Ensure() was called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might just be used to dealing with thread states the subinterpreter way, but I think it could be worth avoiding any int
s entirely in PyThreadState_Ensure
. If we add a new field to PyThreadState
(e.g., ensured
) which is incremented for a call to PyThreadState_Ensure
and then decremented upon a call to PyThreadState_Release
, we could automatically clear and delete the thread state when that number hits zero from PyThreadState_Release
. I think we should discuss this elsewhere, though. I think I'll create the WG discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that you can have nested calls to PyThreadState_Ensure(). How do you store such stack of states in PyThreadState?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking we could set up a field for a linked list, so PyThreadState_Ensure
/PyThreadState_Release
would look something along the lines of:
int
PyThreadState_Ensure(PyInterpreterState *interp)
{
PyThreadState *current = current_fast_get();
if (current == NULL) {
// Make a new thread state for *interp* and return it
/* ... */
}
else {
PyThreadState *new_tstate = _PyThreadState_NewBound(interp);
assert(new_tstate->ensured == NULL);
new_tstate->ensured = current;
_PyThreadState_Detach(current);
/* AttachOrFail(new_tstate) ... */
}
}
void
PyThreadState_Release()
{
PyThreadState *current = current_fast_get();
_Py_EnsureTstateNotNULL(current);
PyThreadState *to_restore = current->ensured;
_PyThreadState_Detach(current);
if (to_restore != NULL) {
/* AttachOrFail(to_restore) */
}
// Nothing to restore, leave no attached tstate
assert(current_fast_get() == NULL);
}
- We probably need a return value for
PyThreadState_Release
because it should be able to reattach to the previous thread state if it didn't have the requested interpreter (regardless of any linked list). - If we go with my linked list idea, it would be nice to have a fast-path which does nothing if there's already an existing thread state that matches interp.
I changed the API to Currently, the only error message is: "Python is being finalized". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this, by the way :)
Add PyThreadState.ensure_depth member.
if (tcur != NULL && tcur->interp != interp) { | ||
// The current thread state is from another interpreter | ||
tcur = NULL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think PyThreadState_Release
is missing code to restore the previous thread state for this case where the active thread state was from a different interpreter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is, but the design is still being discussed. This PR is, more or less, a draft until the C API WG approves it.
I prefer to close this PR for now until we agree on the API semantics. |
Add PyThreadState_Ensure() and PyThreadState_Release() functions.
Add new "OrFail" internal functions:
📚 Documentation preview 📚: https://cpython-previews--130012.org.readthedocs.build/