diff --git a/Include/internal/pycore_frame.h b/Include/internal/pycore_frame.h index 8c410e9e208340..8b40c913d08852 100644 --- a/Include/internal/pycore_frame.h +++ b/Include/internal/pycore_frame.h @@ -49,11 +49,12 @@ typedef enum _framestate { FRAME_SUSPENDED_YIELD_FROM = -1, FRAME_EXECUTING = 0, FRAME_COMPLETED = 1, - FRAME_CLEARED = 4 + FRAME_CLEARED = 4, + FRAME_ZOMBIE = 5, /* For generators left on the stack of cleared threads */ } PyFrameState; #define FRAME_STATE_SUSPENDED(S) ((S) == FRAME_SUSPENDED || (S) == FRAME_SUSPENDED_YIELD_FROM) -#define FRAME_STATE_FINISHED(S) ((S) >= FRAME_COMPLETED) +#define FRAME_STATE_FINISHED(S) ((S) > FRAME_EXECUTING) #ifdef __cplusplus } diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index f93ee54abc6469..7aec61a39d2f75 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -1211,9 +1211,8 @@ async def main(): # can't use assertRaises because that clears frames exc = excs.exceptions[0] self.assertIsNotNone(exc) - self.assertListEqual(gc.get_referrers(exc), [main_coro]) - main_coro = main() - asyncio.run(main_coro) + self.assertListEqual(gc.get_referrers(exc), []) + asyncio.run(main()) if __name__ == '__main__': diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 91f6b03b4597a5..d3175c8b0bf253 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -2,7 +2,6 @@ # license: PSFL. import weakref -import sys import gc import asyncio import contextvars @@ -30,15 +29,6 @@ def get_error_types(eg): return {type(exc) for exc in eg.exceptions} -def no_other_refs(): - # due to gh-124392 coroutines now refer to their locals - coro = asyncio.current_task().get_coro() - frame = sys._getframe(1) - while coro.cr_frame != frame: - coro = coro.cr_await - return [coro] - - def set_gc_state(enabled): was_enabled = gc.isenabled() if enabled: @@ -942,7 +932,7 @@ class _Done(Exception): exc = e self.assertIsNotNone(exc) - self.assertListEqual(gc.get_referrers(exc), no_other_refs()) + self.assertListEqual(gc.get_referrers(exc), []) async def test_exception_refcycles_errors(self): @@ -960,7 +950,7 @@ class _Done(Exception): exc = excs.exceptions[0] self.assertIsInstance(exc, _Done) - self.assertListEqual(gc.get_referrers(exc), no_other_refs()) + self.assertListEqual(gc.get_referrers(exc), []) async def test_exception_refcycles_parent_task(self): @@ -982,7 +972,7 @@ async def coro_fn(): exc = excs.exceptions[0].exceptions[0] self.assertIsInstance(exc, _Done) - self.assertListEqual(gc.get_referrers(exc), no_other_refs()) + self.assertListEqual(gc.get_referrers(exc), []) async def test_exception_refcycles_parent_task_wr(self): @@ -1006,7 +996,7 @@ async def coro_fn(): self.assertIsNone(task_wr()) self.assertIsInstance(exc, _Done) - self.assertListEqual(gc.get_referrers(exc), no_other_refs()) + self.assertListEqual(gc.get_referrers(exc), []) async def test_exception_refcycles_propagate_cancellation_error(self): """Test that TaskGroup deletes propagate_cancellation_error""" @@ -1021,7 +1011,7 @@ async def test_exception_refcycles_propagate_cancellation_error(self): exc = e.__cause__ self.assertIsInstance(exc, asyncio.CancelledError) - self.assertListEqual(gc.get_referrers(exc), no_other_refs()) + self.assertListEqual(gc.get_referrers(exc), []) async def test_exception_refcycles_base_error(self): """Test that TaskGroup deletes self._base_error""" @@ -1038,7 +1028,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): exc = e self.assertIsNotNone(exc) - self.assertListEqual(gc.get_referrers(exc), no_other_refs()) + self.assertListEqual(gc.get_referrers(exc), []) async def test_name(self): name = None diff --git a/Objects/genobject.c b/Objects/genobject.c index 3e7d6257006cfd..325f1ff2e7fcd4 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -63,7 +63,7 @@ gen_traverse(PyObject *self, visitproc visit, void *arg) PyGenObject *gen = _PyGen_CAST(self); Py_VISIT(gen->gi_name); Py_VISIT(gen->gi_qualname); - if (gen->gi_frame_state != FRAME_CLEARED) { + if (gen->gi_frame_state < FRAME_EXECUTING || gen->gi_frame_state == FRAME_ZOMBIE) { _PyInterpreterFrame *frame = &gen->gi_iframe; assert(frame->frame_obj == NULL || frame->frame_obj->f_frame->owner == FRAME_OWNED_BY_GENERATOR); @@ -390,6 +390,7 @@ gen_close(PyObject *self, PyObject *args) if (gen->gi_frame_state == FRAME_CREATED) { gen->gi_frame_state = FRAME_COMPLETED; + _PyFrame_ClearLocals(&gen->gi_iframe); Py_RETURN_NONE; } if (FRAME_STATE_FINISHED(gen->gi_frame_state)) { diff --git a/Python/pystate.c b/Python/pystate.c index 04ca6edb4aaa0e..e53d449bbd41f8 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -9,7 +9,9 @@ #include "pycore_critical_section.h" // _PyCriticalSection_Resume() #include "pycore_dtoa.h" // _dtoa_state_INIT() #include "pycore_emscripten_trampoline.h" // _Py_EmscriptenTrampoline_Init() +#include "pycore_frame.h" // FRAME_ZOMBIE #include "pycore_freelist.h" // _PyObject_ClearFreeLists() +#include "pycore_genobject.h" // _PyGen_GetGeneratorFromFrame() #include "pycore_initconfig.h" // _PyStatus_OK() #include "pycore_interpframe.h" // _PyThreadState_HasStackSpace() #include "pycore_object.h" // _PyType_InitCache() @@ -1623,18 +1625,28 @@ PyThreadState_Clear(PyThreadState *tstate) int verbose = _PyInterpreterState_GetConfig(tstate->interp)->verbose; - if (verbose && tstate->current_frame != NULL) { - /* bpo-20526: After the main thread calls - _PyInterpreterState_SetFinalizing() in Py_FinalizeEx() - (or in Py_EndInterpreter() for subinterpreters), - threads must exit when trying to take the GIL. - If a thread exit in the middle of _PyEval_EvalFrameDefault(), - tstate->frame is not reset to its previous value. - It is more likely with daemon threads, but it can happen - with regular threads if threading._shutdown() fails - (ex: interrupted by CTRL+C). */ - fprintf(stderr, - "PyThreadState_Clear: warning: thread still has a frame\n"); + if (tstate->current_frame != NULL) { + _PyInterpreterFrame *frame = tstate->current_frame; + if (verbose) { + /* bpo-20526: After the main thread calls + _PyInterpreterState_SetFinalizing() in Py_FinalizeEx() + (or in Py_EndInterpreter() for subinterpreters), + threads must exit when trying to take the GIL. + If a thread exit in the middle of _PyEval_EvalFrameDefault(), + tstate->frame is not reset to its previous value. + It is more likely with daemon threads, but it can happen + with regular threads if threading._shutdown() fails + (ex: interrupted by CTRL+C). */ + fprintf(stderr, + "PyThreadState_Clear: warning: thread still has a frame\n"); + } + 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); } if (verbose && tstate->current_exception != NULL) {