Skip to content

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

Closed
wants to merge 11 commits into from

Conversation

vstinner
Copy link
Member

@vstinner vstinner commented Feb 11, 2025

Add PyThreadState_Ensure() and PyThreadState_Release() functions.

Add new "OrFail" internal functions:

  • _PyEval_AcquireLockOrFail()
  • _PyEval_RestoreThreadOrFail()
  • _PyThreadState_AttachOrFail()
  • take_gil_or_fail()

📚 Documentation preview 📚: https://cpython-previews--130012.org.readthedocs.build/

Add PyThreadState_Ensure() and PyThreadState_Release() functions.

Add new "OrFail" internal functions:

* _PyEval_AcquireLockOrFail()
* _PyEval_RestoreThreadOrFail()
* _PyThreadState_AttachOrFail()
* take_gil_or_fail()
Copy link
Member

@ZeroIntensity ZeroIntensity left a 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;
Copy link
Member

@ZeroIntensity ZeroIntensity Feb 11, 2025

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Member Author

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().

Copy link
Member

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)
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

@ZeroIntensity ZeroIntensity Feb 17, 2025

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).

Copy link
Member Author

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.

Copy link
Member

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 ints 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.

Copy link
Member Author

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?

Copy link
Member

@ZeroIntensity ZeroIntensity Feb 27, 2025

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.

@vstinner
Copy link
Member Author

I changed the API to int PyThreadState_Ensure(PyInterpreterState *interp, const char **errmsg): set an error message (if errmsg is not NULL) on error.

Currently, the only error message is: "Python is being finalized".

Copy link
Member

@ZeroIntensity ZeroIntensity left a 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 :)

Comment on lines +2874 to +2876
if (tcur != NULL && tcur->interp != interp) {
// The current thread state is from another interpreter
tcur = NULL;
Copy link
Contributor

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.

Copy link
Member

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.

@vstinner
Copy link
Member Author

I prefer to close this PR for now until we agree on the API semantics.

@vstinner vstinner closed this Mar 20, 2025
@vstinner vstinner deleted the tstate_ensure branch March 20, 2025 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants