GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GH-125603: Don't count executing generators and coroutines as referrers in gc.gc_referrers. by markshannon · Pull Request #125640 · python/cpython

Conversation

@markshannon

@markshannon markshannon commented Oct 17, 2024 •
edited by bedevere-app Bot
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Skipping news as this is reverting behavior that never made it into a release.

  • Issue: [3.14] change in behaviour in gc.get_referrers(some_local) #125603

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@markshannon
Don't count executing generators and coroutines as referrers in gc.ge…
01bf540
…t_referrers.
@markshannon
markshannon requested review from 1st1, asvetlov, gvanrossum, kumaraditya303 and willingc as code owners October 17, 2024 10:54
@bedevere-app bedevere-app Bot added the awaiting core review label Oct 17, 2024
@markshannon markshannon added skip news and removed awaiting core review labels Oct 17, 2024
@bedevere-app bedevere-app Bot mentioned this pull request Oct 17, 2024
[3.14] change in behaviour in gc.get_referrers(some_local) #125603
Open
kumaraditya303
kumaraditya303 approved these changes Oct 17, 2024
View reviewed changes

@kumaraditya303 kumaraditya303 left a comment

Copy link
Copy Markdown
Contributor

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.

asyncio changes LGTM

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@bedevere-app bedevere-app Bot added the awaiting merge label Oct 17, 2024
colesbury
colesbury reviewed Oct 17, 2024
View reviewed changes
Comment thread Objects/genobject.c Outdated
Py_VISIT(gen->gi_name);
Py_VISIT(gen->gi_qualname);
if (gen->gi_frame_state != FRAME_CLEARED) {
if (gen->gi_frame_state < FRAME_EXECUTING) {

@colesbury colesbury Oct 17, 2024

Copy link
Copy Markdown
Contributor

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 this is okay, but it also adds some additional constraints that will make the GC in the free threading build more fragile. I think we have to make sure that any gen/coro marked as FRAME_EXECUTING is on a PyThreadState's frame stack -- otherwise some deferred references may not be visible to the GC and collected while still in use.

  • There must not be any escaping calls between setting gi_frame_state = FRAME_EXECUTING and pushing the frame to the thread's stack.
  • There must not be any escaping calls between popping the frame from the stack and setting gi_frame_state to some other value. (i.e., exit_unwind -> clear_gen_frame)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@markshannon markshannon Oct 17, 2024

Copy link
Copy Markdown
Member Author

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.

All those things are already true, because we rely on gi_frame_state == FRAME_EXECUTING to guard against sending to an already executing generator.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

colesbury reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

@colesbury colesbury Oct 17, 2024

Copy link
Copy Markdown
Contributor

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.

At shutdown we delete all PyThreadStates except for the main thread. Those threads could be running generators or coroutines stuck in some long running call (like a time.sleep()). We run the GC after that. That seems like it would be unsafe (because we're now hiding deferred _PyStackRef from the GC.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@colesbury colesbury Oct 17, 2024

Copy link
Copy Markdown
Contributor

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.

This test program segfaults in the free threading build with this PR:

https://gist.github.com/colesbury/11d59b9987e881a3c016b086bb4ba1ff

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@markshannon markshannon Oct 21, 2024

Copy link
Copy Markdown
Member Author

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 the generator is executing, it is part of the stack, not the heap.
So, I think tp_traverse should not be traversing executing generators.

@colesbury
How does the free-threading GC find references that are on the stack (in normal frames)?
We should do that for executing generators.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@colesbury colesbury Oct 21, 2024

Copy link
Copy Markdown
Contributor

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.

Yes, during shutdown, we delete all PyThreadStates except for the main thread.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@markshannon markshannon Oct 21, 2024

Copy link
Copy Markdown
Member Author

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.

Let me rephrase:
When deleting the thread state, how do we clean up the references?

Presumably, we should be changing the state of the generator when we clean up its frame but we aren't.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@markshannon markshannon Oct 21, 2024

Copy link
Copy Markdown
Member Author

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.

To answer my own question. We don't cleanup the references 🙁

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@colesbury colesbury Oct 21, 2024

Copy link
Copy Markdown
Contributor

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.

We call PyThreadState_Clear() on the deleted thread states, but that doesn't clean up current_frame. We don't call PyThreadState_Delete() for reasons that are not clear to me. Even if PyThreadState_Clear() cleaned up current_frame that wouldn't be sufficient because we unlink all the thread states before calling PyThreadState_Clear() and (by your definition) that already puts them in an invalid state.

This is all longstanding CPython behavior, as far as I can tell. Changing the shutdown behavior seems likely to cause different shutdown-related bugs than what we experience today.

I think we can work around this by encoding more knowledge about generators in gc_free_threading.c. I don't love that, but it seems less risky than messing with the shutdown behavior. Let me know if you want to go that route -- I can help with the gc_free_threading.c changes.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@markshannon markshannon Oct 21, 2024

Copy link
Copy Markdown
Member Author

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 like to investigate fixing up thread state clean up. While it may well risk introducing bugs in the short term, I think to would be better long term. It is hard to optimize anything if we can't trust our supposed invariants.

A quick fix, until we decide on how to handle this long term, would be to traverse the frame list when deleting threads and mark all generators as suspended.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

colesbury reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@markshannon markshannon changed the title GH-125603: Don't count executing generators and coroutines as referrers in gc.gt_referrers. GH-125603: Don't count executing generators and coroutines as referrers in gc.gc_referrers. Oct 22, 2024
@markshannon
Mark generators left on stack when clearing frame as zombies.
9673b8a
@markshannon
markshannon requested a review from ericsnowcurrently as a code owner October 22, 2024 11:10
colesbury
colesbury reviewed Oct 22, 2024
View reviewed changes
Comment thread Python/pystate.c
Comment on lines +1654 to +1660
do {
if (frame->owner == FRAME_OWNED_BY_GENERATOR) {
PyGenObject *gen = _PyGen_GetGeneratorFromFrame(frame);
gen->gi_frame_state = FRAME_ZOMBIE;
}
frame = frame->previous;
} while (frame != NULL);

@colesbury colesbury Oct 22, 2024

Copy link
Copy Markdown
Contributor

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's not sufficient to mark generators as zombies in PyThreadState_Clear(). We call PyThreadState_Clear() one at a time on each non-main thread:

  • At that point, they are already unlinked from the interpreter's list of PyThreadStates
  • The PyThreadState_Clear() calls makes escaping calls (Py_CLEAR()) that can trigger the GC

So the GC may still see generators that are marked as "running", but aren't in any accessible PyThreadState's frame stack.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@colesbury colesbury Jul 28, 2025

Copy link
Copy Markdown
Contributor

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 wrote "At that point, they are already unlinked from the interpreter's list of PyThreadStates", but I don't think that's accurate. Thread states are unlinked in PyThreadState_Delete or PyThreadState_DeleteCurrent, and those happen after PyThreadState_Clear.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@colesbury colesbury Jul 28, 2025

Copy link
Copy Markdown
Contributor

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... my Oct 22, 2024 comment was in relation to interpreter shutdown, which reverses the typical order. During shutdown thread states are:

  1. Unlinked
  2. Cleared
  3. Deleted

Whereas during normal thread exit the order is:

  1. Cleared
  2. Unlinked
  3. Deleted

Specifically, in _Py_Finalize() we:

  1. First we unlink all the remaining thread states (except the main thread). This is typically for daemon threads, but if you press Ctrl-C it can include non-daemon threads as well:

cpython/Python/pylifecycle.c

Line 2079 in 1150321

PyThreadState *list = _PyThreadState_RemoveExcept(tstate);

  1. Then we call _PyThreadState_DeleteList(), which calls PyThreadState_Clear() on each thread one at a time, which can execute arbitrary code via finalizers and weakrefs.

cpython/Python/pylifecycle.c

Line 2090 in 1150321

_PyThreadState_DeleteList(list, /*is_after_fork=*/0);

cpython/Python/pystate.c

Lines 1904 to 1922 in 1150321

void
_PyThreadState_DeleteList(PyThreadState *list, int is_after_fork)
{
// The world can't be stopped because we PyThreadState_Clear() can
// call destructors.
assert(!_PyRuntime.stoptheworld.world_stopped);
PyThreadState *p, *next;
for (p = list; p; p = next) {
next = p->next;
PyThreadState_Clear(p);
if (is_after_fork) {
free_threadstate((_PyThreadStateImpl *)p);
}
else {
decref_threadstate((_PyThreadStateImpl *)p);
}
}
}

I think things should be okay if we mark the generators as zombies from _PyThreadState_RemoveExcept() as well as from PyThreadState_Clear().

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@kumaraditya303 kumaraditya303 mentioned this pull request Jan 7, 2025
gh-128552: fix refcycles in eager task creation #128553
Merged
@graingert graingert mentioned this pull request Jan 25, 2025
simplify test_asyncio.test_taskgroups no_other_refs #129284
Open
@musicinmybrain musicinmybrain mentioned this pull request Feb 13, 2025
Python 3.14: test_ki_protection_doesnt_leave_cyclic_garbage fails python-trio/trio#3209
Closed
@markshannon
Merge branch 'main' into hide-executing-generator-refs
daa080f
@markshannon
markshannon requested a review from ZeroIntensity as a code owner July 28, 2025 11:45
markshannon added 3 commits July 28, 2025 12:54
@markshannon
Add missing includes
3ba315f
@markshannon
Update test
6b34f05
@markshannon
Keep ruff happy
a59fa5e
@markshannon

markshannon commented Aug 1, 2025

Copy link
Copy Markdown
Member Author

Unlinking a thread before clearing seems strange. Why do we do that during interpreter shutdown?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@colesbury

colesbury commented Aug 1, 2025

Copy link
Copy Markdown
Contributor

It looks like we've been doing it this way since 3.9:

  1. Looping over the interpreter's linked list of thread states requires HEAD_LOCK()
  2. It's not safe to call PyThreadState_Clear() inside of HEAD_LOCK()

Prior to 3.9 we called PyThreadState_Clear() inside of HEAD_LOCK(), which has it's own problems.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@sergey-miryanov

sergey-miryanov commented Oct 25, 2025

Copy link
Copy Markdown
Contributor

Is there any way I can help with this?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions

github-actions Bot commented Apr 19, 2026

Copy link
Copy Markdown

This PR is stale because it has been open for 30 days with no activity.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions github-actions Bot added the stale Stale PR or inactive for long period of time. label Apr 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@colesbury colesbury colesbury left review comments
@kumaraditya303 kumaraditya303 kumaraditya303 approved these changes
@1st1 1st1 Awaiting requested review from 1st1 1st1 is a code owner
@asvetlov asvetlov Awaiting requested review from asvetlov asvetlov is a code owner
@gvanrossum gvanrossum Awaiting requested review from gvanrossum
@willingc willingc Awaiting requested review from willingc willingc is a code owner
@ericsnowcurrently ericsnowcurrently Awaiting requested review from ericsnowcurrently ericsnowcurrently is a code owner
@ZeroIntensity ZeroIntensity Awaiting requested review from ZeroIntensity ZeroIntensity is a code owner

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

awaiting merge skip news stale Stale PR or inactive for long period of time.

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

4 participants

@markshannon @colesbury @sergey-miryanov @kumaraditya303
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.