diff --git a/Include/internal/pycore_frame.h b/Include/internal/pycore_frame.h index c9ac3819d0390b..f476bb51f659d4 100644 --- a/Include/internal/pycore_frame.h +++ b/Include/internal/pycore_frame.h @@ -46,11 +46,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) enum _frameowner { FRAME_OWNED_BY_THREAD = 0, diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index dbe5646c2b7c08..272a39441d6414 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -1214,9 +1214,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 1b4de96a572fb9..f1de555e55ad35 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -29,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] - - class TestTaskGroup(unittest.IsolatedAsyncioTestCase): async def test_taskgroup_01(self): @@ -923,7 +914,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): @@ -941,7 +932,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): @@ -963,7 +954,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_propagate_cancellation_error(self): """Test that TaskGroup deletes propagate_cancellation_error""" @@ -978,7 +969,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""" @@ -995,7 +986,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): exc = e self.assertIsNotNone(exc) - self.assertListEqual(gc.get_referrers(exc), no_other_refs()) + self.assertListEqual(gc.get_referrers(exc), []) if __name__ == "__main__": diff --git a/Objects/genobject.c b/Objects/genobject.c index 19c2c4e3331a89..118a5643d6e45e 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -58,7 +58,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); @@ -375,6 +375,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 e3812cba41d9c2..9ee26d82d64a51 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1636,18 +1636,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); } /* At this point tstate shouldn't be used any more,