Skip to content

gh-149101: Implement PEP 788#149116

Open
ZeroIntensity wants to merge 81 commits intopython:mainfrom
ZeroIntensity:pep-788
Open

gh-149101: Implement PEP 788#149116
ZeroIntensity wants to merge 81 commits intopython:mainfrom
ZeroIntensity:pep-788

Conversation

@ZeroIntensity
Copy link
Copy Markdown
Member

@ZeroIntensity ZeroIntensity commented Apr 28, 2026

Hugo has graciously given me permission to backport this if we don't make the May 5th deadline, but let's try to get this done in time!

I will write a full tutorial and migration guide once this is merged; I want to first make sure that this lands before the beta freeze.

@read-the-docs-community
Copy link
Copy Markdown

read-the-docs-community Bot commented Apr 28, 2026

Copy link
Copy Markdown
Member

@encukou encukou 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 adding these!

I'll send notes for Doc/ now; code review coming up.

Comment thread Doc/c-api/interp-lifecycle.rst Outdated
Comment thread Doc/c-api/interp-lifecycle.rst Outdated
Comment thread Doc/c-api/interp-lifecycle.rst Outdated
Comment thread Doc/c-api/interp-lifecycle.rst
Comment thread Doc/c-api/interp-lifecycle.rst Outdated
Comment thread Doc/c-api/threads.rst Outdated
Comment thread Doc/c-api/threads.rst Outdated
Comment thread Doc/c-api/threads.rst Outdated
Comment thread Doc/c-api/threads.rst Outdated
Comment thread Doc/whatsnew/3.15.rst Outdated
ZeroIntensity and others added 5 commits April 29, 2026 08:24
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
@ZeroIntensity ZeroIntensity added the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Apr 29, 2026
@bedevere-bot
Copy link
Copy Markdown

🤖 New build scheduled with the buildbot fleet by @ZeroIntensity for commit bc78c10 🤖

Results will be shown at:

https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F149116%2Fmerge

If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again.

@bedevere-bot bedevere-bot removed the 🔨 test-with-buildbots Test PR w/ buildbots; report in status section label Apr 29, 2026
@encukou
Copy link
Copy Markdown
Member

encukou commented Apr 30, 2026

for buildbots: The RHEL8 failures aren't relevant. Refleaks are worrying though.

@encukou
Copy link
Copy Markdown
Member

encukou commented Apr 30, 2026

Refleaks are worrying though.

Never mind; main currently leaks (#149179).

Comment thread Python/pylifecycle.c Outdated
Claude pointed this out as an issue. I think it makes some sense.
We cannot safely release a guard until PyThreadState_Delete() has been
called, otherwise _PyThreadState_DeleteList can see an invalid thread
state in the list. For example:

1. Thread A starts waiting on guards.
2. Thread B calls PyThreadState_Release(), which clears its thread state
   (making it invalid), and then releases the guard.
3. Thread B's thread state is still on the tstate list!
4. Thread A begins finalization now that there are no guards left.
5. Thread A calls _PyThreadState_DeleteList, which iterates over the
   thread state list, and then finds thread B's thread state.
6. Thread A tries to call PyThreadState_Clear() on thread B's thread
   state, but it has already been cleared!
7. Kaboom.
Comment thread Python/pylifecycle.c
@@ -2330,17 +2370,28 @@ make_pre_finalization_calls(PyThreadState *tstate, int subinterpreters)
int has_subinterpreters = subinterpreters
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think all these checks here should be done atomically without acquiring/releasing locks in between. So basically:

PyMutex_Lock(&interp->ceval.pending.mutex);
_PyEval_StopTheWorldAll(interp->runtime);
HEAD_LOCK(&_PyRuntime);
// ... do the checks and compare-exchange on finalization_guards
HEAD_UNLOCK(_PyRuntime);
_PyEval_StartTheWorldAll(interp->runtime);
PyMutex_Unlock(&interp->ceval.pending.mutex);

In other words, lift the HEAD_LOCK() out from runtime_has_subinterpreters and interp_has_threads

Comment thread Include/pystate.h Outdated

// CPython detail: this struct is not defined anywhere; tokens are special
// sentinels or PyThreadState's.
typedef struct PyThreadStateToken PyThreadStateToken;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I stuck the pattern in ChatGPT and it agreed with @encukou that it's not UB, at least if struct PyThreadStateToken is kept opaque.

The typedef struct PyThreadStateToken PyThreadStateToken definition is fine with me, and will probably provide stricter type checking.

Comment thread Python/pystate.c Outdated
Comment thread Python/pylifecycle.c Outdated
_PyThreadState_Detach(tstate);
_PyEval_StartTheWorldAll(interp->runtime);
PyMutex_Unlock(&interp->ceval.pending.mutex);
_PyThreadState_Attach(tstate);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the current ordering here deliberate? I'm not sure if there's any issues, but the split of detach/attach across _PyEval_StartTheWorldAll and PyMutex_Unlock confuses me.

I would write it as:

        _PyEval_StartTheWorldAll(interp->runtime);
        PyMutex_Unlock(&interp->ceval.pending.mutex);

        // Temporarily let other threads execute
        _PyThreadState_Detach(tstate);
        _PyThreadState_Attach(tstate);

But it's also not clear to me whey we need _PyThreadState_Detach/Attach here. If other threads are running, won't we wait on them in either wait_for_thread_shutdown or when parking on the finalization_guards?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, I think that's an artifact of an older variation.

Comment thread Python/pylifecycle.c Outdated
Comment thread Python/pystate.c Outdated
Comment on lines +3486 to +3487
++attached_tstate->ensure.counter;
return NO_TSTATE_SENTINEL;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is a bug here (found by Claude) where PyThreadState_Ensure/PyThreadState_Release with an already attached detaches on release when it should keep the active thread state.

In other words, we aren't properly distinguishing the two cases "already has a valid active thread state" and "doesn't have any attached thread state".

https://gist.github.com/colesbury/b6538312a898dd97fdd770b22fb3c338

Comment thread Doc/c-api/interp-lifecycle.rst Outdated
Comment thread Doc/c-api/interp-lifecycle.rst Outdated
Failure indicates that the process is out of memory or that the main
interpreter has finalized (or never existed).

Using this function in extension libraries is strongly discouraged, because
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should use an affirmative tone. For example:

Use this function when an interpreter pointer or view cannot be supplied by the caller, for example, when a native threading library does not provide a void *arg parameter that could carry a :c:type:PyInterpreterGuard or :c:type:PyInterpreterView. In code that supports subinterpreters, prefer :c:func:PyInterpreterView_FromCurrent so the guard tracks the calling interpreter rather than the main one.

Comment thread Include/internal/pycore_lock.h Outdated
// Yield the processor to other threads (e.g., sched_yield).
extern void _Py_yield(void);
// Exported for _testembed.
PyAPI_FUNC(void) _Py_yield(void);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure I understand the purpose of this in the tests. If the intent is to try to capture timing bugs, use something like pysleep. You can copy-paste those few lines of code if necessary.

I'd prefer we don't export this symbol since that complicates potential future bug fixes if we need to change or remove it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll just remove it. My hope was that this would make some of the apparent thread safety issues reproduce more reliably, but it doesn't seem that it did.

Comment thread Python/pystate.c
if (detached_gilstate != NULL && detached_gilstate->interp == interp) {
/* There's a detached thread state that works. */
assert(attached_tstate == NULL);
++detached_gilstate->ensure.counter;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this also has a bug. Let's say you have:

t1 = PyThreadState_Ensure(main_interp_guard); // counter 0 -> 1
Py_BEGIN_ALLOW_THREADS
t2 = PyThreadState_Ensure(main_interp_guard); // counter 1 -> 2
PyThreadState_Release(t2); // BUG! 2->1
Py_END_ALLOW_THREADS
PyThreadState_Release(t1);

The inner PyThreadState_Release should restore the thread state to "not attached", but it leaves it as attached due to the counter.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hm, this looks tricky to solve. I guess we could turn PyThreadStateToken into an actual structure that holds the necessary state, but I'm not super excited about adding another heap allocation to PyThreadState_Ensure. I'll have to think about this one a little bit.

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