--- /dev/null
+.. 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.
+