From cb371d913d05d5bb789b1d4de3974eb30559f0e2 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 26 May 2026 13:48:02 +0530 Subject: [PATCH 1/3] add asyncio guide for free-threading --- Doc/library/asyncio-threading.rst | 142 ++++++++++++++++++++++++++++++ Doc/library/asyncio.rst | 1 + 2 files changed, 143 insertions(+) create mode 100644 Doc/library/asyncio-threading.rst diff --git a/Doc/library/asyncio-threading.rst b/Doc/library/asyncio-threading.rst new file mode 100644 index 000000000000000..aa0c9e06cf206fd --- /dev/null +++ b/Doc/library/asyncio-threading.rst @@ -0,0 +1,142 @@ +.. currentmodule:: asyncio + +.. _asyncio-threading: + +asyncio and Free-Threaded Python +================================ + +asyncio uses an event loop as a scheduler to enable highly efficient +I/O-bound concurrency by switching between tasks during non-blocking I/O +operations. It allows off-loading CPU-bound work to a thread or process +pool, but that is still limited by the :term:`global interpreter lock` +in CPython. + +However, with :pep:`703`, Python can now be built in a "free-threaded" +mode, which removes the GIL and allows true multi-threading. This means +that asyncio can now take advantage of multiple CPU cores without the +limitations imposed by the GIL. + +Since, Python 3.14 asyncio has first class support for free-threaded +Python, and the implementation of asyncio is safe to use in a +multi-threaded environment. + +.. seealso:: + + `Scaling asyncio on Free-Threaded Python + `_, + a blog post by Kumar Aditya which explains through the internal changes + that make asyncio safe and efficient under free-threaded Python, + together with benchmarks of the resulting improvements. + + +Thread Safety Considerations +---------------------------- + +While asyncio is designed to be thread-safe in a free-threaded Python +environment, there are still some considerations to keep in mind when +using asyncio with threads: + +1. **Event Loop**: Each thread should have its own event loop which + should not be shared across threads. This ensures that the event loop + can manage its own tasks and callbacks without interference from + other threads. + +2. **Task Management**: Tasks and futures created in one thread should + not be awaited or manipulated from another thread. + +3. **Thread-Safe APIs**: When interacting with asyncio from multiple + threads, it's important to use thread-safe APIs provided by asyncio, + such as :func:`asyncio.run_coroutine_threadsafe` for submitting + coroutines to an event loop from another thread. If you need to + call a callback from a different thread, you can use + :meth:`loop.call_soon_threadsafe` to schedule it safely. + +4. **Synchronization**: The synchronization primitives provided by + asyncio (like :class:`asyncio.Lock`, :class:`asyncio.Event`, etc.) + are not designed to be used across threads. If you need to + synchronize between threads, you should use the synchronization + primitives from the :mod:`threading` module instead. + + +Using asyncio with Threads +-------------------------- + +asyncio supports running one event loop per thread, which allows you to +take advantage of multiple CPU cores in a free-threaded Python +environment. Each thread can run its own event loop, and tasks can be +scheduled on those loops independently. + +Here's an example of how to use asyncio with threads:: + + import asyncio + import threading + + async def worker(name: str) -> None: + print(f"Worker {name} starting") + await asyncio.sleep(1) + print(f"Worker {name} done") + + def run_loop(name: str) -> None: + asyncio.run(worker(name)) + + threads = [ + threading.Thread(target=run_loop, args=(f"T{i}",)) + for i in range(4) + ] + for t in threads: + t.start() + for t in threads: + t.join() + +In this example, each thread creates its own event loop with +:func:`asyncio.run` and runs a coroutine on it. The threads execute +concurrently, and in a free-threaded build they can run on separate +CPU cores in parallel. + + +Producer/Consumer Across Threads +-------------------------------- + +When a regular (non-asyncio) thread needs to hand work to an asyncio +event loop running in another thread, use a thread-safe primitive such +as :class:`queue.Queue` rather than :class:`asyncio.Queue`, which is +only safe within a single event loop.:: + + import asyncio + import queue + import threading + + def producer(q: queue.Queue[int]) -> None: + for i in range(5): + print(f"Producing {i}") + q.put(i) + q.shutdown() + + async def consumer(q: queue.Queue[int]) -> None: + while True: + try: + item = q.get_nowait() + except queue.Empty: + await asyncio.sleep(0.1) + continue + except queue.ShutDown: + break + print(f"Consumed {item}") + await asyncio.sleep(item) + + q: queue.Queue[int] = queue.Queue() + consumer_thread = threading.Thread( + target=lambda: asyncio.run(consumer(q)) + ) + consumer_thread.start() + producer(q) + consumer_thread.join() + +The producer runs on the main thread while the consumer runs inside an +event loop on its own thread, yet they communicate safely through +``queue.Queue``. When the queue is empty the consumer sleeps briefly +and tries again. When the producer is done it calls +:meth:`~queue.Queue.shutdown`, which causes subsequent +:meth:`~queue.Queue.get_nowait` calls to raise :exc:`queue.ShutDown` +so the consumer can exit cleanly. + diff --git a/Doc/library/asyncio.rst b/Doc/library/asyncio.rst index 0f72e31dee5f1d1..90a465f3e1d3af4 100644 --- a/Doc/library/asyncio.rst +++ b/Doc/library/asyncio.rst @@ -128,6 +128,7 @@ for full functionality and the latest features. asyncio-api-index.rst asyncio-llapi-index.rst asyncio-dev.rst + asyncio-threading.rst .. note:: The source code for asyncio can be found in :source:`Lib/asyncio/`. From 2aa483626d1c29cf653080b6321c4debe9d975f5 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 26 May 2026 15:31:32 +0530 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Doc/library/asyncio-threading.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Doc/library/asyncio-threading.rst b/Doc/library/asyncio-threading.rst index aa0c9e06cf206fd..2ca64530f066ecc 100644 --- a/Doc/library/asyncio-threading.rst +++ b/Doc/library/asyncio-threading.rst @@ -16,35 +16,35 @@ mode, which removes the GIL and allows true multi-threading. This means that asyncio can now take advantage of multiple CPU cores without the limitations imposed by the GIL. -Since, Python 3.14 asyncio has first class support for free-threaded +Since Python 3.14, asyncio has first-class support for free-threaded Python, and the implementation of asyncio is safe to use in a multi-threaded environment. .. seealso:: `Scaling asyncio on Free-Threaded Python - `_, - a blog post by Kumar Aditya which explains through the internal changes + `__, + a blog post by Kumar Aditya which explains the internal changes that make asyncio safe and efficient under free-threaded Python, together with benchmarks of the resulting improvements. -Thread Safety Considerations +Thread safety considerations ---------------------------- While asyncio is designed to be thread-safe in a free-threaded Python environment, there are still some considerations to keep in mind when using asyncio with threads: -1. **Event Loop**: Each thread should have its own event loop which +1. **Event loop**: Each thread should have its own event loop which should not be shared across threads. This ensures that the event loop can manage its own tasks and callbacks without interference from other threads. -2. **Task Management**: Tasks and futures created in one thread should +2. **Task management**: Tasks and futures created in one thread should not be awaited or manipulated from another thread. -3. **Thread-Safe APIs**: When interacting with asyncio from multiple +3. **Thread-safe APIs**: When interacting with asyncio from multiple threads, it's important to use thread-safe APIs provided by asyncio, such as :func:`asyncio.run_coroutine_threadsafe` for submitting coroutines to an event loop from another thread. If you need to @@ -52,13 +52,13 @@ using asyncio with threads: :meth:`loop.call_soon_threadsafe` to schedule it safely. 4. **Synchronization**: The synchronization primitives provided by - asyncio (like :class:`asyncio.Lock`, :class:`asyncio.Event`, etc.) + asyncio (like :class:`asyncio.Lock` and :class:`asyncio.Event`) are not designed to be used across threads. If you need to synchronize between threads, you should use the synchronization primitives from the :mod:`threading` module instead. -Using asyncio with Threads +Using asyncio with threads -------------------------- asyncio supports running one event loop per thread, which allows you to @@ -94,7 +94,7 @@ concurrently, and in a free-threaded build they can run on separate CPU cores in parallel. -Producer/Consumer Across Threads +Producer/consumer across threads -------------------------------- When a regular (non-asyncio) thread needs to hand work to an asyncio From 308ce2b6dc8322b2c9a40b25726494ff215e3f5b Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 26 May 2026 15:31:51 +0530 Subject: [PATCH 3/3] Update Doc/library/asyncio-threading.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Doc/library/asyncio-threading.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-threading.rst b/Doc/library/asyncio-threading.rst index 2ca64530f066ecc..baeb50e9cad472d 100644 --- a/Doc/library/asyncio-threading.rst +++ b/Doc/library/asyncio-threading.rst @@ -2,7 +2,7 @@ .. _asyncio-threading: -asyncio and Free-Threaded Python +asyncio and free-threaded Python ================================ asyncio uses an event loop as a scheduler to enable highly efficient