mirror of
https://github.com/python/cpython.git
synced 2026-06-22 19:12:50 -04:00
155 lines
5.5 KiB
ReStructuredText
155 lines
5.5 KiB
ReStructuredText
.. 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.
|
|
|