Урок 08 из 12 · 2 минуты чтения

asyncio — корутины, задачи, отмена и блокирующий код

Как работают async и await, как запускать корутины одновременно, gather и TaskGroup, что делает блокирующий вызов в цикле событий, отмена и таймауты.

Корутины и цикл событий

async def создаёт корутину. await — точка, где она отдаёт управление циклу событий, пока ждёт сеть, диск или таймер. Цикл в это время выполняет другие корутины — всё в одном потоке.

import asyncio, httpx

async def fetch(client, url):
    resp = await client.get(url, timeout=5)
    return resp.status_code

async def main():
    async with httpx.AsyncClient() as client:
        print(await fetch(client, "https://example.ru"))

asyncio.run(main())

asyncio.run создаёт цикл событий и выполняет корутину. Вызов корутины без await ничего не делает — Python предупредит «coroutine was never awaited».

Одновременно, а не по очереди

a = await fetch(client, url1)   # по очереди —
b = await fetch(client, url2)   # выигрыша нет

a, b = await asyncio.gather(fetch(client, url1), fetch(client, url2))

gather запускает корутины конкурентно и возвращает результаты в том же порядке. С Python 3.11 есть TaskGroup:

async with asyncio.TaskGroup() as tg:
    t1 = tg.create_task(fetch(client, url1))
    t2 = tg.create_task(fetch(client, url2))
print(t1.result(), t2.result())

Разница — в ошибках. gather пробрасывает первую ошибку, но остальные задачи продолжают работать брошенными. TaskGroup отменяет остальные, ждёт их завершения и отдаёт ошибки группой ExceptionGroup — задач-сирот не остаётся.

Ограничить число одновременных запросов — asyncio.Semaphore.

Блокирующий вызов останавливает всё

Корутины переключаются только в точках await. Блокирующий вызов — time.sleep, requests.get, синхронный драйвер базы, тяжёлое вычисление — не отдаёт управление, и весь цикл событий стоит. Симптом: растут задержки всех запросов, хотя процессор почти не загружен.

Решения:

  • асинхронные библиотеки — httpx, asyncpg, aiofiles;
  • вынести блокирующий код в поток: await asyncio.to_thread(func, arg);
  • тяжёлые вычисления — в пул процессов через loop.run_in_executor.

Отладочный режим asyncio предупреждает о шагах дольше 100 мс. Во FastAPI обработчик def выполняется в пуле потоков, а async def — прямо в цикле, поэтому блокирующий код в async def особенно опасен.

Отмена и таймауты

task.cancel() бросает CancelledError внутрь корутины в ближайшей точке await. Таймаут:

try:
    async with asyncio.timeout(3):
        data = await fetch_slow()
except TimeoutError:
    data = None

По истечении времени задача отменяется, наружу приходит TimeoutError.

CancelledError нельзя глотать: очистить ресурсы в finally или async with и пробросить дальше. С Python 3.8 он наследует BaseException, поэтому except Exception его не ловит — так и задумано. Синхронный код без await отменить нельзя: отмена сработает только на следующем await. asyncio.shield защищает внутреннюю операцию от отмены внешней.

Что запомнить

  • await — точка переключения; без него корутины не чередуются.
  • Одновременный запуск — gather или TaskGroup, а не await подряд.
  • TaskGroup отменяет соседей при ошибке.
  • Блокирующий вызов останавливает весь цикл — to_thread или асинхронная библиотека.
  • CancelledError пробрасывают, а не глотают.

Проверка урока

Войдите, чтобы проверить себя: сданные проверки всех уроков курса отмечают его темы пройденными на карте и в ИПР.

Вопросы по теме

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

Партнёр платформы

Разбор собеседования с sotto

После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.

Разбор собеседования