| .. 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. | |