Сообщить об ошибке.

Асинхронные функции (async def), понимание сопрограмм в Python

Содержание:

Асинхронные функции (async def)

Асинхронные функции (async def) не запускаются сразу: вызов возвращает объект-сопрограмму. Код внутри стартует только при await или когда корутину оборачивают в Task и планируют в event loop. Внутри async def можно использовать await, async for, async with, а результат и исключения возвращаются через await так же, как в обычных функциях через return и raise.

Что такое асинхронная функция

async def func():
    return 42

Такая запись не делает функцию асинхронной "по поведению" сразу.

Важно:

  • async def определяет функцию-корутину (coroutine function);
  • вызов func() не запускает код сразу, а создаёт объект-сопрограмму (coroutine object).
coro = func() # код внутри ещё не выполнялся

Код внутри начнёт выполняться только при:

  • await coro, или
  • когда сопрограмма будет запланирована в event loop (например, через asyncio.create_task).

Отличие от обычной функции

Обычная функция:

  • вызывается => выполняется до конца => возвращает значение или выбрасывает исключение.

Асинхронная функция:

  • вызывается => возвращает объект-сопрограмму;
  • дальнейшее поведение зависит от того, когда и как эту сопрограмму ожидают (await).

Пример:

def normal():
    return 1

async def async_func():
    return 2

x = normal() # x == 1
y = async_func() # y - coroutine object, не число

Чтобы получить 2:

result = await async_func()

или в более полном виде:

import asyncio

async def main():
    result = await async_func()
    print(result)

asyncio.run(main())

Что можно внутри async def

Внутри асинхронной функции можно использовать:

  • await - ожидание другого awaitable;
  • async for - асинхронная итерация;
  • async with - асинхронные контекстные менеджеры;
  • обычные синхронные конструкции (if/for/while, try/except и т.п.).

await ожидает awaitable - объект, который реализует протокол __await__ или является другой корутиной/задачей.

async def handler():
    data = await fetch_data()
    async for item in stream_items():
        await process_item(item)

Возврат значений и исключения

Асинхронная функция логически похожа на обычную по семантике return / исключений:

  • return value внутри async def => при await получаем value;
  • необработанное исключение внутри async def всплывает при await.
async def f():
    return 10

async def g():
    raise ValueError("error")

async def main():
    print(await f()) # 10
    print(await g()) # возбуждается ValueError

asyncio.run(main())

Связь с event loop и задачами

Сопрограмму (объект, полученный из async def) обычно не вызывают напрямую из синхронного кода. Нужен цикл событий (asyncio):

  • верхний уровень программы: asyncio.run(main());
  • внутри main создаются и ожидаются другие корутины.

Для параллельного выполнения нескольких сопрограмм используется Task:

async def worker(name):
    await asyncio.sleep(1)
    print(f"done {name}")

async def main():
    t1 = asyncio.create_task(worker("A"))
    t2 = asyncio.create_task(worker("B"))
    await t1
    await t2

asyncio.run(main())

create_task:

  • берёт корутину,
  • оборачивает её в Task,
  • регистрирует в event loop,
  • loop сам будет возобновлять её выполнение каждый раз, когда она готова продолжить.

Внутреннее устройство асинхронной функции

На уровне CPython (упрощённо):

  • async def компилируется в coroutine function;
  • её вызов создаёт объект типа coroutine, который:
    • содержит код (code),
    • хранит кадр (frame) с локальными переменными,
    • реализует протокол __await__.

При await coro:

  1. Интерпретатор вызывает coro.__await__(), получая итератор.
  2. Event loop "крутит" этот итератор (по сути, похож на yield from).
  3. Генерация StopIteration с value => завершение сопрограммы с этим результатом.
  4. Исключения => всплывают вверх в тот, кто делает await.

Исторически await во многом эквивалентен yield from для специальных "async" объектов, но с более строгой типизацией и отдельным байткодом.

Практические рекомендации по асинхронным функциям

  1. Асинхронные функции объявляем async def, когда внутри:
    • нужно использовать await;
    • или функция логически является частью асинхронного API.
  2. Всегда дожидаемся корутин:
    • через await;
    • или через asyncio.create_task + последующий await задачи;
    • избегаем "висящих" корутин, которые нигде не awaited.
  3. На верхнем уровне приложения используем asyncio.run(main()) и держим всю логику в async def main().

Понимание сопрограмм (корутин)

Сопрограмма - это "приостанавливаемая функция": она выполняется до ближайшего await, отдаёт управление циклу событий и позже продолжается с того же места. Это даёт кооперативную многозадачность: переключения происходят только там, где явно есть await. В отличие от потоков, корутины лёгкие, управляются на уровне Python, хорошо подходят для I/O-bound задач, поддерживают отмену (CancelledError) и требуют аккуратного cleanup через try/finally и/или async-контекстные менеджеры.

Что такое сопрограмма концептуально

Сопрограмма - это единица вычисления, которая:

  • может приостанавливаться в определённых точках;
  • отдавать управление "снаружи";
  • потом возобновляться с того же места, сохраняя локальное состояние.

В Python:

  • синхронные генераторы (def + yield) - тоже вид сопрограмм;
  • асинхронные функции (async def) - native coroutines (современный механизм).

Главная идея:

Сопрограммы дают кооперативную многозадачность: они явно говорят, когда готовы "уступить" управление.

Виды корутин в Python

В современных версиях Python основным механизмом являются:

  • native coroutines - объекты, создаваемые async def.

Есть также устаревший механизм:

  • generator-based coroutines - через @asyncio.coroutine + yield from (используется всё реже, оставлен в основном для обратной совместимости).

Мы фокусируемся на native coroutines.

Awaitable и протокол __await__

Сопрограмма - частный случай awaitable. Awaitable - это объект, который можно передать в await:

  • native coroutine (результат async def);
  • asyncio.Task;
  • любой объект, у которого есть метод __await__, возвращающий итератор.

Когда пишем:

result = await some_awaitable

внутри происходит:

  1. Вызывается some_awaitable.__await__() => получаем итератор.
  2. Event loop "крутит" этот итератор, ожидая, пока тот либо:
    • вернёт результат (через StopIteration со значением),
    • либо поднимет исключение.

Корутины async def предоставляют корректную реализацию __await__ автоматически.

Жизненный цикл сопрограммы

У любой корутины есть типичный жизненный цикл:

  1. Создание
    • Вызов async def создаёт coroutine object, но код ещё не выполнялся.
  2. Планирование / старт
    • либо напрямую через await coro в другой корутине;
    • либо через asyncio.create_task(coro).
  3. Выполнение
    • идём по коду до первого await, где управление отдаётся event loop;
    • при возобновлении продолжаем после await.
  4. Завершение
    • при return => результат доставляется через await;
    • при исключении => исключение всплывает в await.

В отличие от потока:

  • у сопрограммы нет собственного стека ОС;
  • переходы выполняются добровольно через await, а не принудительно планировщиком ОС.

Отличие корутин от потоков

Ключевые отличия:

  1. Потоки:
    • планируются ОС, переключение может происходить в любой момент;
    • нужна синхронизация (lock, semaphore) для общего состояния;
    • переключение тяжеловеснее (контекст потока).
  2. Сопрограммы:
    • планируются event loop на уровне приложения;
    • переключение происходит только в точках await;
    • легче контролировать состояние: пока нет await, код выполняется "атомарно" относительно других корутин.

Практически:

  • корутины хорошо подходят для I/O-bound задач с большим количеством ожиданий;
  • потоки - для CPU-bound, либо там, где нужна настоящая параллельность.

Взаимодействие корутин между собой

Часто строится "пирамидальная" структура:

async def low_level():
    ...

async def mid_level():
    result = await low_level()
    ...

async def high_level():
    await mid_level()

Каждый await:

  • "делегирует" выполнение вложенной корутине;
  • создаёт цепочку зависимостей: ошибка в low_level всплывёт через mid_level в high_level.

Можно рассматривать это как асинхронный call stack.

Cancelation (отмена)

Сопрограммы могут быть отменены:

  • например, при task.cancel() для asyncio.Task.

При отмене:

  1. Внутрь корутины вбрасывается исключение asyncio.CancelledError в ближайшей точке ожидания (await).
  2. Если оно не перехвачено, корутина завершается с этим исключением.
  3. await task снаружи тоже получит CancelledError.

Поэтому внутри длительных корутин:

  • часто используют try/finally для корректного cleanup (освобождение ресурсов, закрытие соединений и т.п.);
  • по аналогии с синхронным кодом, но с учётом, что finally может содержать await.

Внутреннее устройство корутин (концептуально)

Упрощённо:

  • coroutine object содержит:
    • ссылку на код,
    • текущую позицию в коде,
    • локальные переменные,
    • состояние (создана, выполняется, завершена).

Event loop:

  • регистрирует задачами (Task) набор корутин;
  • каждая задача выполняется до ближайшего await на операции I/O;
  • когда операция I/O завершилась, loop снова запускает соответствующую корутину (продолжая с того места, где она остановилась).

То есть корутина - это "приостанавливаемая функция", а event loop - менеджер, который решает, какую корутину и когда возобновить.