From 1deb2e31e4e3dd4217ce8d578bdd7fd23eaa816f Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Wed, 24 Jun 2026 15:50:55 +0300 Subject: [PATCH] [3.14] gh-152020: Fix `asyncio.all_tasks()` loosing eager tasks on FT-build (#152022) --- Lib/test/test_asyncio/test_free_threading.py | 39 +++++++++++++++++++ ...-06-23-19-50-22.gh-issue-152020.DTKXjR.rst | 3 ++ Modules/_asynciomodule.c | 11 +++--- 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst diff --git a/Lib/test/test_asyncio/test_free_threading.py b/Lib/test/test_asyncio/test_free_threading.py index d874ed00bd7e7a..0e149dadd7f121 100644 --- a/Lib/test/test_asyncio/test_free_threading.py +++ b/Lib/test/test_asyncio/test_free_threading.py @@ -165,6 +165,45 @@ async def main(): loop.set_task_factory(self.factory) r.run(main()) + def test_all_tasks_from_other_thread_includes_eager_tasks(self): + # gh-152020: all_tasks() called from another thread used to drop + # eager-started tasks on free-threaded builds. + loop = asyncio.new_event_loop() + + async def wait_forever(): + await asyncio.Event().wait() + + def eager_factory(loop, coro, **kwargs): + return self.factory(loop, coro, eager_start=True, **kwargs) + + async def setup(): + loop.set_task_factory(eager_factory) + eager = loop.create_task(wait_forever(), name="EAGER") + loop.set_task_factory(None) + normal = loop.create_task(wait_forever(), name="NORMAL") + return eager, normal + + async def teardown(): + tasks = [t for t in asyncio.all_tasks() + if t is not asyncio.current_task()] + for t in tasks: + t.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + + thread = threading.Thread(target=loop.run_forever) + thread.start() + try: + held = asyncio.run_coroutine_threadsafe(setup(), loop).result() + names = {t.get_name() for t in asyncio.all_tasks(loop)} + self.assertIn("NORMAL", names) + self.assertIn("EAGER", names) + del held + finally: + asyncio.run_coroutine_threadsafe(teardown(), loop).result() + loop.call_soon_threadsafe(loop.stop) + thread.join() + loop.close() + class TestPyFreeThreading(TestFreeThreading, TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst new file mode 100644 index 00000000000000..93c716f7a6a1c8 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst @@ -0,0 +1,3 @@ +On the free-threaded build, :func:`asyncio.all_tasks` no longer loses +eager-started tasks when called from a thread other than the one running the +event loop. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 1a4a41ef6fc9d5..46a55f8b91d387 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -2385,7 +2385,11 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop, if (self->task_name == NULL) { return -1; } - +#ifdef Py_GIL_DISABLED + // This is required so that _Py_TryIncref(self) + // works correctly in non-owning threads. + _PyObject_SetMaybeWeakref((PyObject *)self); +#endif if (eager_start) { PyObject *res = PyObject_CallMethodNoArgs(loop, &_Py_ID(is_running)); if (res == NULL) { @@ -2404,11 +2408,6 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop, if (task_call_step_soon(state, self, NULL)) { return -1; } -#ifdef Py_GIL_DISABLED - // This is required so that _Py_TryIncref(self) - // works correctly in non-owning threads. - _PyObject_SetMaybeWeakref((PyObject *)self); -#endif register_task(self); return 0; }