asyncio в Python: async, await и цикл событий
Продвинутое3 минРедакция Innovialabs
Коротко
asyncio позволяет одному потоку обслуживать много операций ввода-вывода, пока они ждут сеть или диск; для вычислений на CPU он не ускоряет код — там нужны процессы.
Суть в одном абзаце
asyncio — это способ выполнять много операций ожидания в одном потоке. Пока один запрос ждёт ответа от сети, цикл событий переключается на другой. Код не становится быстрее сам по себе — он просто перестаёт простаивать в ожидании. Поэтому asyncio выигрывает на вводе-выводе (HTTP, базы данных, сокеты) и ничего не даёт на тяжёлых вычислениях.
Ключевые понятия
- Корутина — функция, объявленная через
async def. Её вызов не выполняет код, а создаёт объект корутины. await— точка, где корутина говорит: «я жду, можно заняться другим». Переключение происходит только в таких точках.- Цикл событий (event loop) — планировщик, который решает, какую корутину продолжить. Запускается через
asyncio.run(). - Задача (Task) — корутина, поставленная в цикл на выполнение параллельно с остальными. Создаётся через
asyncio.create_task(). gather— запускает несколько корутин конкурентно и ждёт все результаты.
Минимальный пример
import asyncio
async def fetch(i: int) -> int:
await asyncio.sleep(1) # имитация сетевого запроса
return i
async def main() -> None:
results = await asyncio.gather(*(fetch(i) for i in range(3)))
print(results) # [0, 1, 2]
asyncio.run(main())
Три «запроса» по одной секунде выполнятся примерно за одну секунду, а не за три: все три ожидания идут одновременно.
Задачи: запустить и продолжить работу
async def main() -> None:
task = asyncio.create_task(fetch(1))
# здесь можно делать другую работу
result = await task
Храните ссылку на созданную задачу и обязательно дожидайтесь её. Задача без ссылок может быть удалена сборщиком мусора до завершения — об этом прямо предупреждает документация Python.
Когда async ускоряет, а когда нет
| Сценарий | Эффект от asyncio |
|---|---|
| Много HTTP-запросов к API | Значительный: ожидания перекрываются |
| Telegram-бот, веб-сервер, вебсокеты | Высокий: много соединений на одном потоке |
| Запросы к базе через async-драйвер | Есть, если драйвер действительно асинхронный |
| Обработка изображений, ML, парсинг больших файлов | Нет: CPU занят, переключаться некуда |
| Один последовательный запрос | Нет: ждать нечего параллельно |
Для CPU-задач используйте процессы (ProcessPoolExecutor, multiprocessing) или выносите вычисления в отдельные сервисы и очереди.
Главная ловушка: блокирующие вызовы
Цикл событий работает в одном потоке. Если внутри корутины вызвать что-то синхронное и долгое, встанет всё приложение:
time.sleep()вместоawait asyncio.sleep();- синхронные HTTP-клиенты вроде
requestsвместо асинхронных; - синхронные драйверы баз данных;
- тяжёлые вычисления прямо в обработчике.
Если синхронную библиотеку заменить нельзя, отправьте вызов в отдельный поток:
data = await asyncio.to_thread(blocking_function, arg)
Другие частые ошибки
- Забыли
await. Корутина не выполнится, Python выдаст предупреждение «coroutine was never awaited». - Неограниченная конкурентность. Тысячи одновременных запросов могут упереться в лимиты API или исчерпать соединения. Ограничивайте через
asyncio.Semaphore. - Ошибки в
gather. По умолчанию первое исключение пробрасывается сразу. Если нужны все результаты, используйтеreturn_exceptions=Trueи проверяйте каждый. - Вызов
asyncio.run()внутри работающего цикла. Внутри корутин используйтеawait, а не новый запуск цикла.