]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
add asyncio guide for Free-Threaded Python (#150456)
authorKumar Aditya <kumaraditya@python.org>
Wed, 10 Jun 2026 14:02:11 +0000 (19:32 +0530)
committerGitHub <noreply@github.com>
Wed, 10 Jun 2026 14:02:11 +0000 (14:02 +0000)
Doc/library/asyncio-threading.rst [new file with mode: 0644]
Doc/library/asyncio.rst

diff --git a/Doc/library/asyncio-threading.rst b/Doc/library/asyncio-threading.rst
new file mode 100644 (file)
index 0000000..526901a
--- /dev/null
@@ -0,0 +1,154 @@
+.. currentmodule:: asyncio
+
+.. _asyncio-threading:
+
+asyncio and free-threaded Python
+================================
+
+asyncio uses an event loop as a scheduler to enable highly efficient
+concurrency by switching between tasks to allow non-blocking I/O
+operations. This results in better performance for I/O-bound use
+cases. It also 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, in :ref:`free-threaded Python <freethreading-python-howto>`,
+the GIL is disabled and Python can run true multi-threaded code. 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.
+
+A single event loop on one core can handle many connections
+concurrently, but the Python code that runs to handle each one still
+executes serially. Once requests involve a non-trivial amount of
+per-request computation, that handling becomes the bottleneck, and a
+single core can no longer keep up. Combining asyncio with threads is
+most useful here: by running an event loop per thread, the handling of
+different requests can run in parallel across multiple CPU cores. It is
+also useful when you need to run blocking or CPU-bound code from an
+asyncio application.
+
+
+.. seealso::
+
+   `Scaling asyncio on Free-Threaded Python
+   <https://labs.quansight.org/blog/scaling-asyncio-on-free-threaded-python>`__,
+   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
+----------------------------
+
+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` 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
+--------------------------
+
+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.
+
index 0f72e31dee5f1d13b20be4cd2078598d9c92905b..90a465f3e1d3af46184ff4d317bed9a510e307b6 100644 (file)
@@ -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/`.