diff --git a/Lib/test/test_free_threading/test_asyncio.py b/Lib/test/test_free_threading/test_asyncio.py new file mode 100644 index 000000000000000..c1cdfb015efe7fd --- /dev/null +++ b/Lib/test/test_free_threading/test_asyncio.py @@ -0,0 +1,48 @@ +import asyncio +import threading +import unittest +from unittest import TestCase + +from test.support import threading_helper + + +async def _forever(): + await asyncio.Event().wait() + + +@threading_helper.requires_working_threading() +class TestAllTasks(TestCase): + 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 setup(): + loop.set_task_factory(asyncio.eager_task_factory) + loop.create_task(_forever(), name="EAGER") + loop.set_task_factory(None) + loop.create_task(_forever(), name="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: + 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) + finally: + asyncio.run_coroutine_threadsafe(teardown(), loop).result() + loop.call_soon_threadsafe(loop.stop) + thread.join() + loop.close() + + +if __name__ == "__main__": + unittest.main() 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 000000000000000..659fbdb2b7e156c --- /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 lost +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 6620ee26449b163..31223ac894e6c08 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -2190,6 +2190,9 @@ register_task(_PyThreadStateImpl *ts, TaskObj *task) assert(task->task_node.prev != NULL); return; } +#ifdef Py_GIL_DISABLED + _PyObject_SetMaybeWeakref((PyObject *)task); +#endif struct llist_node *head = &ts->asyncio_tasks_head; llist_insert_tail(head, &task->task_node); }