From 8e2864aa4670c7b0b95cc6eee9ca2a97d3f86a37 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Tue, 23 Jun 2026 19:51:34 +0300 Subject: [PATCH 1/9] gh-152020: Fix asyncio.all_tasks() loosing eager tasks on FT-build --- Lib/test/test_free_threading/test_asyncio.py | 48 +++++++++++++++++++ ...-06-23-19-50-22.gh-issue-152020.DTKXjR.rst | 3 ++ Modules/_asynciomodule.c | 3 ++ 3 files changed, 54 insertions(+) create mode 100644 Lib/test/test_free_threading/test_asyncio.py 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_free_threading/test_asyncio.py b/Lib/test/test_free_threading/test_asyncio.py new file mode 100644 index 00000000000000..c1cdfb015efe7f --- /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 00000000000000..659fbdb2b7e156 --- /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 6620ee26449b16..31223ac894e6c0 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); } From 9a8733b08b19d492d1b21ba8442c0dda913e6fcb Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Tue, 23 Jun 2026 20:10:53 +0300 Subject: [PATCH 2/9] cc --- Lib/test/test_free_threading/test_asyncio.py | 48 +++++++++++++++++++ ...-06-23-19-50-22.gh-issue-152020.DTKXjR.rst | 3 ++ Modules/_asynciomodule.c | 8 ++-- 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 Lib/test/test_free_threading/test_asyncio.py create mode 100644 Misc/NEWS.d/next/Library/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst 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 00000000000000..c1cdfb015efe7f --- /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/Library/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst b/Misc/NEWS.d/next/Library/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst new file mode 100644 index 00000000000000..659fbdb2b7e156 --- /dev/null +++ b/Misc/NEWS.d/next/Library/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 6620ee26449b16..9a257111930753 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); } @@ -2384,11 +2387,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(ts, self); return 0; } From dfa20f9b8cacedebbd0c4fad1a5fb3669d23d3a4 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Wed, 24 Jun 2026 13:01:02 +0300 Subject: [PATCH 3/9] mark task before it runs eagerly --- Modules/_asynciomodule.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 9a257111930753..57c8d4a41a3a0d 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -2190,9 +2190,6 @@ 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); } @@ -2369,6 +2366,11 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop, return -1; } _PyThreadStateImpl *ts = (_PyThreadStateImpl *)_PyThreadState_GET(); +#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) { From 52fe06a5edfa1a53bcab5964ee908613236f7141 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Wed, 24 Jun 2026 13:04:27 +0300 Subject: [PATCH 4/9] removed dup --- Modules/_asynciomodule.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 20ae975c0b9e6f..57c8d4a41a3a0d 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -2190,9 +2190,6 @@ 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); } From af269b96ca3c3103ece3b930ef086a9e2c3b7892 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Wed, 24 Jun 2026 13:05:25 +0300 Subject: [PATCH 5/9] removed duplicate NEWS entry --- .../Library/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst diff --git a/Misc/NEWS.d/next/Library/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst b/Misc/NEWS.d/next/Library/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst deleted file mode 100644 index 659fbdb2b7e156..00000000000000 --- a/Misc/NEWS.d/next/Library/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst +++ /dev/null @@ -1,3 +0,0 @@ -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 From f2c6fa15332a2d841c8d73ee03e00dc6bf02ac39 Mon Sep 17 00:00:00 2001 From: Timofei <128279579+deadlovelll@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:11:18 +0300 Subject: [PATCH 6/9] Update Misc/NEWS.d/next/Core_and_Builtins/2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst Co-authored-by: Kumar Aditya --- .../2026-06-23-19-50-22.gh-issue-152020.DTKXjR.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 659fbdb2b7e156..93c716f7a6a1c8 100644 --- 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 @@ -1,3 +1,3 @@ -On the free-threaded build, :func:`asyncio.all_tasks` no longer lost +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 +event loop. From 2fb44548017552a17b5d01854b6f358e9dea4a71 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Wed, 24 Jun 2026 13:46:10 +0300 Subject: [PATCH 7/9] moved test from test_free_threading/test_asyncio to test_asyncio/test_free_threading --- Lib/test/test_asyncio/test_free_threading.py | 39 ++++++++++++++++ Lib/test/test_free_threading/test_asyncio.py | 48 -------------------- 2 files changed, 39 insertions(+), 48 deletions(-) delete mode 100644 Lib/test/test_free_threading/test_asyncio.py diff --git a/Lib/test/test_asyncio/test_free_threading.py b/Lib/test/test_asyncio/test_free_threading.py index d874ed00bd7e7a..803099e6210dfd 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 _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(_forever(), name="EAGER") + loop.set_task_factory(None) + normal = loop.create_task(_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/Lib/test/test_free_threading/test_asyncio.py b/Lib/test/test_free_threading/test_asyncio.py deleted file mode 100644 index c1cdfb015efe7f..00000000000000 --- a/Lib/test/test_free_threading/test_asyncio.py +++ /dev/null @@ -1,48 +0,0 @@ -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() From b14b49e4a4be0dd94f2d79d9fafcda5e575a6192 Mon Sep 17 00:00:00 2001 From: Timofei <128279579+deadlovelll@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:54:10 +0300 Subject: [PATCH 8/9] Update Lib/test/test_asyncio/test_free_threading.py Co-authored-by: Kumar Aditya --- Lib/test/test_asyncio/test_free_threading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_free_threading.py b/Lib/test/test_asyncio/test_free_threading.py index 803099e6210dfd..c46f0e0b9dcd05 100644 --- a/Lib/test/test_asyncio/test_free_threading.py +++ b/Lib/test/test_asyncio/test_free_threading.py @@ -170,7 +170,7 @@ def test_all_tasks_from_other_thread_includes_eager_tasks(self): # eager-started tasks on free-threaded builds. loop = asyncio.new_event_loop() - async def _forever(): + async def wait_forever(): await asyncio.Event().wait() def eager_factory(loop, coro, **kwargs): From 143c9e141b8fd8f447a24a74e7996789cab5c475 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Wed, 24 Jun 2026 14:01:17 +0300 Subject: [PATCH 9/9] fix _forever calls to wait_forever --- Lib/test/test_asyncio/test_free_threading.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_asyncio/test_free_threading.py b/Lib/test/test_asyncio/test_free_threading.py index c46f0e0b9dcd05..0e149dadd7f121 100644 --- a/Lib/test/test_asyncio/test_free_threading.py +++ b/Lib/test/test_asyncio/test_free_threading.py @@ -178,9 +178,9 @@ def eager_factory(loop, coro, **kwargs): async def setup(): loop.set_task_factory(eager_factory) - eager = loop.create_task(_forever(), name="EAGER") + eager = loop.create_task(wait_forever(), name="EAGER") loop.set_task_factory(None) - normal = loop.create_task(_forever(), name="NORMAL") + normal = loop.create_task(wait_forever(), name="NORMAL") return eager, normal async def teardown():