PythonMiddleTechnical

Что такое корутина?

Корутина — это объект, представляющий приостанавливаемое вычисление. async def возвращает coroutine object, который не выполняется сам по себе — его нужно await-ить, обернуть в Task или запустить через asyncio.run, иначе появится warning "coroutine was never awaited".

Определение

Корутина в Python — обобщённая функция, выполнение которой можно приостанавливать в точках await и возобновлять позже. Технически это объект типа types.CoroutineType, у него есть методы send(), throw(), close() — как у генератора, но он не реализует __iter__ и не годится для for.

Создание и запуск

import asyncio

async def compute(x):           # это coroutine function
    await asyncio.sleep(0.1)
    return x * 2

coro = compute(21)              # это coroutine object, тело ещё НЕ выполнялось

async def main():
    result = await coro         # вот здесь выполняется
    print(result)               # 42

asyncio.run(main())

Если сделать только compute(21) и ничего с этим объектом не сделать, Python выдаст RuntimeWarning: coroutine 'compute' was never awaited.

Coroutine vs Task vs Future

  • Coroutine — описание работы, само по себе не исполняется.
  • Task — subclass Future, обёртка вокруг coroutine, которая планирует её на event loop через asyncio.create_task(coro). Создание Task сразу же ставит coroutine на исполнение.
  • Future — placeholder результата без привязки к корутине, его выставляет, например, low-level транспорт.

Native vs generator-based

До Python 3.5 корутины писали через @asyncio.coroutine и yield from. С 3.5 появился async def / await, а в 3.10 legacy-форма официально удалена. Современный код пишут только через native syntax.

Запуск нескольких

async def main():
    # последовательно — 0.3 сек
    a = await compute(1)
    b = await compute(2)
    # параллельно — 0.1 сек
    a, b = await asyncio.gather(compute(1), compute(2))
    # с TaskGroup (3.11+) — корректная отмена при ошибке
    async with asyncio.TaskGroup() as tg:
        t1 = tg.create_task(compute(1))
        t2 = tg.create_task(compute(2))

Подводные камни

  • Забыли await — coroutine не исполнилась, warning, баг тихий.
  • Считать, что await запускает несколько корутин одновременно — нет, это последовательное ожидание.
  • Запуск asyncio.run() внутри уже запущенного loop — RuntimeError: asyncio.run() cannot be called from a running event loop.
  • Coroutine не является thread-safe в смысле shared state — синхронизируйтесь через asyncio.Lock.
  • Smell: блокирующие вызовы (time.sleep, requests) внутри coroutine блокируют весь loop.

Common mistakes

  • Говорить, что async def сразу выполняется.
  • Не различать coroutine object и результат.
  • Не знать warning never awaited.

What the interviewer is testing

  • Объясняет вызов async def.
  • Понимает await.
  • Различает coroutine и Task на базовом уровне.

Sources

Related topics