asyncio
| Тип | модуль стандартной библиотеки |
|---|---|
| Язык | Python |
| Появился в | Python 3.4 (2014) |
| Основные PEP | PEP 3156, PEP 492 |
| Модель | однопоточный цикл событий, кооперативная многозадачность |
| Альтернативы | Trio, AnyIO, gevent |
| Документация | docs.python.org/3/library/asyncio |
asyncio — модуль стандартной библиотеки Python для написания конкурентного кода с помощью синтаксиса async/await. Он реализует цикл событий (event loop), который в одном потоке переключается между множеством задач, пока те ждут ввода-вывода: ответа сети, базы данных, файла. asyncio лежит в основе асинхронных веб-фреймворков (FastAPI, aiohttp), драйверов баз данных и многих клиентов API.
История
До asyncio асинхронный ввод-вывод в Python обеспечивали сторонние библиотеки — Twisted, Tornado, gevent — с несовместимыми между собой моделями. В 2012 году Гвидо ван Россум предложил единый стандарт (PEP 3156, кодовое имя Tulip), и в Python 3.4 (2014) модуль вошёл в стандартную библиотеку.[2] Первоначально сопрограммы писались на генераторах с yield from.
В Python 3.5 появились ключевые слова async def и await (PEP 492),[3] сделав сопрограммы отдельной сущностью языка. Дальнейшие важные изменения:
- 3.7 — функция
asyncio.run(), ставшая стандартной точкой входа; - 3.9 —
asyncio.to_thread()для вызова блокирующего кода в потоке; - 3.11 —
TaskGroupиasyncio.timeout(), структурированная конкурентность.[4]
Основные понятия
Сопрограмма (coroutine) — функция, объявленная через async def. Её вызов не выполняет код, а возвращает объект-сопрограмму; выполнение начинается, когда её ожидают через await или оборачивают в задачу.
Цикл событий — планировщик, который хранит готовые к выполнению задачи и следит за сокетами через механизмы ОС (epoll, kqueue, IOCP). Когда задача доходит до await на незавершённой операции, она добровольно отдаёт управление, и цикл запускает следующую. Это кооперативная многозадачность: переключение происходит только в точках await.
Задача (Task) — сопрограмма, запущенная в цикле событий конкурентно с другими (asyncio.create_task()).
Future — низкоуровневый объект «результат появится позже»; задачи являются его подклассом.
Пример
import asyncio
import time
async def fetch(name: str, delay: float) -> str:
await asyncio.sleep(delay) # имитация сетевого запроса
return f"{name}: готово за {delay} с"
async def main() -> None:
start = time.perf_counter()
async with asyncio.TaskGroup() as tg: # Python 3.11+
tasks = [tg.create_task(fetch(f"запрос {i}", 1.0)) for i in range(3)]
for t in tasks:
print(t.result())
print(f"всего: {time.perf_counter() - start:.1f} с") # ≈ 1.0, а не 3.0
asyncio.run(main())
Три «запроса» по одной секунде выполняются примерно за секунду, потому что ожидание перекрывается. Если одна из задач в TaskGroup завершится исключением, остальные будут отменены, а ошибки собраны в ExceptionGroup.
Инструменты модуля
| Средство | Назначение |
|---|---|
asyncio.gather() |
дождаться нескольких awaitable и собрать результаты |
asyncio.TaskGroup |
структурированный запуск группы задач с отменой при ошибке |
asyncio.timeout(), wait_for() |
ограничение времени ожидания |
asyncio.Queue |
очередь производитель — потребитель |
asyncio.Semaphore, Lock, Event |
примитивы синхронизации |
asyncio.to_thread() |
выполнить блокирующую функцию в отдельном потоке |
loop.run_in_executor() |
то же для пула потоков или процессов |
потоки (open_connection) |
высокоуровневая работа с TCP |
Типичные ошибки
- Блокирующий вызов внутри сопрограммы.
time.sleep(), синхронный HTTP-клиент или тяжёлый расчёт останавливают весь цикл событий. Их нужно заменять асинхронными аналогами или выносить вto_thread(). - Забытый
await. Вызов сопрограммы безawaitничего не делает; Python выдаёт предупреждение «coroutine was never awaited». - Потерянные задачи. Задача, созданная
create_task()без сохранения ссылки, может быть удалена сборщиком мусора; документация рекомендует хранить ссылки или использоватьTaskGroup.[1] - Ожидание ускорения расчётов. asyncio не распараллеливает вычисления на ядра процессора — для этого нужны процессы или сборка без GIL (см. GIL и free-threading (PEP 703)).
Режим отладки (asyncio.run(main(), debug=True) или переменная окружения PYTHONASYNCIODEBUG=1) сообщает о медленных обратных вызовах и неожиданных сопрограммах.[5]
asyncio, потоки и процессы
| Подход | Когда подходит |
|---|---|
| asyncio | тысячи одновременных сетевых соединений, I/O-нагрузка |
потоки (threading) |
умеренное число блокирующих операций, совместимость со старыми библиотеками |
процессы (multiprocessing) |
вычисления на нескольких ядрах |
Подробнее о различии понятий — в статье Конкурентность и параллелизм.
Альтернативы
Trio предложил идею «детских садов» (nurseries) — предшественника TaskGroup. AnyIO даёт единый API поверх asyncio и Trio. uvloop — замена стандартного цикла событий на основе libuv, ускоряющая сетевой код.