Spec-Zone.ru › Python 3.14

asyncio и Python со свободной многопоточностью

asyncio использует цикл событий в качестве планировщика и обеспечивает высокоэффективную параллельную обработку, переключаясь между задачами и позволяя выполнять неблокирующие операции ввода-вывода. Это повышает производительность в сценариях, ограниченных скоростью ввода-вывода. Кроме того, можно передавать задачи, ограниченные производительностью процессора, пулу потоков или процессов, однако в CPython это по-прежнему ограничено глобальной блокировкой интерпретатора.

Однако в Python со свободной многопоточностью GIL отключена, и Python может выполнять настоящий многопоточный код. Это означает, что теперь asyncio может использовать несколько ядер процессора без ограничений, накладываемых GIL.

Начиная с Python 3.14 asyncio изначально поддерживает Python со свободной многопоточностью, а реализацию asyncio безопасно использовать в многопоточной среде.

Один цикл событий на одном ядре может одновременно обрабатывать множество соединений, однако код Python, выполняемый для обработки каждого из них, по-прежнему выполняется последовательно. Когда обработка запросов начинает требовать нетривиальных вычислений для каждого запроса, она становится узким местом, и одного ядра уже недостаточно. В таких случаях особенно полезно сочетать asyncio с потоками: если запустить цикл событий в каждом потоке, обработка разных запросов сможет выполняться параллельно на нескольких ядрах процессора. Это также полезно, когда из приложения asyncio требуется запускать блокирующий код или код, ограниченный производительностью процессора.

См. также

Масштабирование asyncio в Python со свободной многопоточностью — статья в блоге Кумара Адитьи, в которой объясняются внутренние изменения, обеспечивающие безопасность и эффективность asyncio в Python со свободной многопоточностью, а также приводятся результаты тестов производительности.

Особенности потокобезопасности

Хотя asyncio спроектирован с учетом потокобезопасности в среде Python со свободной многопоточностью, при использовании asyncio с потоками следует помнить о некоторых особенностях:

  1. Цикл событий: у каждого потока должен быть собственный цикл событий, который нельзя использовать совместно с другими потоками. Это позволяет циклу событий управлять собственными задачами и обратными вызовами без вмешательства других потоков.
  2. Управление задачами: задачи и объекты future, созданные в одном потоке, нельзя ожидать или изменять из другого потока.
  3. Потокобезопасные API: при взаимодействии с asyncio из нескольких потоков важно использовать предоставляемые asyncio потокобезопасные API, например asyncio.run_coroutine_threadsafe() для передачи сопрограмм циклу событий из другого потока. Если нужно вызвать обратный вызов из другого потока, можно безопасно запланировать его с помощью loop.call_soon_threadsafe().
  4. Синхронизация: примитивы синхронизации asyncio (такие как asyncio.Lock и asyncio.Event) не предназначены для использования между потоками. Для синхронизации потоков следует вместо них использовать примитивы синхронизации из модуля threading.

Использование asyncio с потоками

asyncio поддерживает запуск одного цикла событий в каждом потоке, что позволяет использовать несколько ядер процессора в среде Python со свободной многопоточностью. Каждый поток может выполнять собственный цикл событий, а задачи можно независимо планировать в этих циклах.

Пример использования asyncio с потоками:

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()

В этом примере каждый поток создает собственный цикл событий с помощью asyncio.run() и выполняет в нем сопрограмму. Потоки выполняются одновременно, а в сборке со свободной многопоточностью могут параллельно работать на разных ядрах процессора.

Производитель и потребитель в разных потоках

Если обычному потоку (не asyncio) нужно передать работу циклу событий asyncio, выполняющемуся в другом потоке, используйте потокобезопасный примитив, например queue.Queue, а не asyncio.Queue, который безопасно использовать только в пределах одного цикла событий:

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()

Производитель работает в главном потоке, а потребитель — внутри цикла событий в отдельном потоке, но они безопасно обмениваются данными через queue.Queue. Когда очередь пуста, потребитель ненадолго засыпает и пробует снова. После завершения работы производитель вызывает shutdown(). В результате последующие вызовы get_nowait() вызывают исключение queue.ShutDown, что позволяет потребителю корректно завершить работу.

© 2001 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/library/asyncio-threading.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API