Корутины, async/await и событийный цикл

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

Корутина — функция, объявленная через async def, выполнение которой можно приостанавливать в точках await и возобновлять позже. Событийный цикл (event loop) — планировщик, который решает, какую корутину продолжить.

Анатомия корутины

Корутина объявляется ключевым словом async. Вызов fetch() не запускает её сразу — он возвращает объект-корутину, который нужно отдать событийному циклу.

import asyncio

async def fetch(name, delay):
    print(f"{name}: старт")
    await asyncio.sleep(delay)   # точка приостановки
    print(f"{name}: готово через {delay}с")
    return name

async def main():
    result = await fetch("Запрос", 1)
    print("Результат:", result)

asyncio.run(main())

Этот пример помечен как иллюстративный: в браузерной песочнице asyncio.run() не работает, потому что событийный цикл там уже запущен. На реальной машине вывод был бы таким:

Запрос: старт
Запрос: готово через 1с
Результат: Запрос

Что делает await

Слово await говорит циклу: «эта операция будет ждать — забери у меня управление и займись чем-нибудь полезным, а когда мой результат будет готов, верни управление сюда». Именно в точках await происходит переключение между корутинами.

Важно: ждать (await) можно только awaitable-объекты — другие корутины, задачи (Task) или future. Обычную блокирующую функцию (например, time.sleep) внутри корутины ставить нельзя — она заблокирует весь цикл, и смысл асинхронности теряется. Для пауз есть asyncio.sleep.

Роль событийного цикла

Цикл — это бесконечный планировщик: он держит очередь готовых к работе корутин, выполняет одну до ближайшего await, запоминает, чего она ждёт, и переключается на следующую. Когда ожидаемое событие наступает (таймер истёк, пришёл ответ), цикл возвращается к приостановленной корутине.

asyncio.run(main()) делает три вещи: создаёт событийный цикл, выполняет в нём корутину main() до конца и закрывает цикл.

Кооперативность: модель на генераторах

Чтобы прочувствовать суть переключения без asyncio, смоделируем кооперативный планировщик обычными генераторами. Каждая «корутина» делает шаг и уступает управление через yield — ровно как настоящая уступает его на await.

def task(name, steps):
    for i in range(1, steps + 1):
        yield f"{name}: шаг {i}/{steps}"   # точка уступки управления

def scheduler(tasks):
    tasks = list(tasks)
    while tasks:
        still_running = []
        for t in tasks:
            try:
                print(next(t))          # продвигаем задачу на один шаг
                still_running.append(t)
            except StopIteration:
                pass                     # задача завершилась
        tasks = still_running

scheduler([task("A", 3), task("B", 2)])

Вывод:

A: шаг 1/3
B: шаг 1/2
A: шаг 2/3
B: шаг 2/2
A: шаг 3/3

Обратите внимание на чередование A и B — это и есть конкурентность: задачи продвигаются по очереди, каждая уступает после своего шага. Настоящий событийный цикл устроен сложнее (ждёт реальные I/O-события), но идея переключения в точках уступки та же самая.

Итог

  • async def создаёт корутину; её вызов возвращает объект, а не запускает код.
  • await отдаёт управление событийному циклу и помечает точку переключения.
  • Событийный цикл — планировщик, переключающий корутины в точках await; запускается через asyncio.run().
Проверьте себя
1. Что делает ключевое слово await внутри корутины?
AСоздаёт новый поток операционной системы
BУступает управление событийному циклу до готовности результата
CПолностью блокирует программу на время ожидания
DЗапускает корутину на отдельном ядре процессора
2. Что вернёт прямой вызов корутины async def fetch() без await и без цикла?
AГотовый результат функции
BОбъект-корутину, который ещё не выполнялся
CОшибку синтаксиса
DNone
3. Почему внутрь корутины нельзя ставить блокирующий time.sleep?
Atime.sleep не существует в Python
BОн заблокирует весь событийный цикл, и другие корутины встанут
CОн работает только в потоках
DОн автоматически превращается в await
Поддержать проект