mirror of
https://github.com/python/cpython.git
synced 2026-06-28 03:41:13 +00:00
[3.15] add asyncio guide for Free-Threaded Python (GH-150456) (#151257)
add asyncio guide for Free-Threaded Python (GH-150456)
(cherry picked from commit e2bd50d2e1)
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
This commit is contained in:
parent
7a76730125
commit
637746d6fc
2 changed files with 155 additions and 0 deletions
154
Doc/library/asyncio-threading.rst
Normal file
154
Doc/library/asyncio-threading.rst
Normal file
|
|
@ -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.
|
||||
|
||||
|
|
@ -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/`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue