Перейти к содержанию
Innovialabs

asyncio в Python: async, await и цикл событий

Продвинутое3 мин

Коротко

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, а не новый запуск цикла.

Частые вопросы

Нет. Обычно всё выполняется в одном потоке, а переключение происходит только в точках await. Это конкурентность, а не параллельное выполнение на нескольких ядрах.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.