Async Python: event loop, async/await, asyncio. Что под капотом?

Async‑Python строится вокруг событийного цикла (event loop), который в одном потоке по очереди запускает корутины и возвращает управление туда, где те делают await — в этот момент цикл может запланировать и выполнить другие задачи. async/await — это синтаксис для корутин: функция async def возвращает объект‑корутину, а await временно приостанавливает её и отдаёт управление циклу; под капотом это тот же протокол, что и у генераторов (__await__, send, StopIteration). asyncio — это реализация этого подхода в стандартной библиотеке: event loop + задачи (Task), будущие результаты (Future), высокоуровневые примитивы (gather, sleep, клиенты, серверы) и обвязка вокруг системного неблокирующего I/O (select/epoll/kqueue).

Подробный ответ

Вся асинхронность крутится вокруг одного объекта — event loop (событийный цикл). Он делает две вещи: 

  1. запускает async def‑функции (корутины);

  2. переключается между ними, когда они чего‑то ждут (сетевой ответ, таймер и т.п.), чтобы в это время выполнялся другой код.

Важно: это всё происходит в одном потоке и с тем же GIL. Асинхронность даёт нам одновременное ожидание многих I/O‑операций, а не параллельные вычисления на всех ядрах.

async/await простыми словами

async def — объявляет асинхронную функцию (корутину), а не обычную функцию. Вызов такой функции (например, назовём её coro) записью coro() возвращает объект‑корутину, а не готовый результат; чтобы получить результат, нужно либо написать await coro() внутри другой async‑функции, либо передать эту корутину в asyncio.create_task(coro()), чтобы её выполнил событийный цикл.

await — это место, где корутина приостанавливается.

Когда интерпретатор встречает await something:

  • текущая корутина говорит event loop’у: «я жду something»;

  • управление возвращается в event loop;

  • event loop в это время запускает другие корутины или ждёт I/O.

Пока корутина выполняется между await‑ами, она блокирует event loop сама, поэтому внутри async‑кода нельзя долго крутить чистый CPU‑код без await — остальные задачи будут стоять.

Что такое Future и Task

Чтобы event loop мог понимать, что уже завершилось, а что ещё выполняется, используются объекты Future

Future — это контейнер для результата, который появится позже: либо значения, либо исключения. У него есть состояние: «ожидание», «успешно завершён», «завершён с ошибкой».

asyncio.Task — это обёртка над корутиной, которая одновременно является и задачей (Task), и Future. Task запускает корутину и по завершении кладёт её результат или ошибку внутрь себя как в Future;

await task означает «подождать, пока задача завершится, и получить её результат».

То есть:

корутина — это «что делать» (код);

Future — «коробка» для результата;

Task = «корутина + Future» в одном объекте, другими словами, обёртка вокруг корутины.

Event loop по шагам

Цикл событий (asyncio‑loop) делает примерно следующее:

  1. Хранит список задач (Task).

  2. Берёт задачу, продвигает её корутину до ближайшего await.

  3. Если корутина упёрлась в await asyncio.sleep(), ожидание сети и т.п., задача помечается как «ждущая», и цикл запускает следующую задачу.

  4. Внутри у цикла есть ожидание событий ОС (select/epoll/kqueue и аналоги) — там он ждёт:

  • срабатывание таймеров;

  • появления данных на сокетах или готовности к записи;

  • перехода связанных Future в состояние «готов» (например, завершения других задач).

Как только нужное событие произошло (например, пришли данные по сети или истёк таймер), event loop:

  • помечает соответствующий Future как завершённый;

  • возвращает связанную с ним задачу в очередь «готовых»;

  • продолжает её корутину с места await.

Ключевой момент: переключение кооперативное — корутина сама отдаёт управление, но только когда делает await.

Зачем нужен asyncio?

asyncio — это стандартная библиотека, которая даёт реализацию событийного цикла; умеет превращать корутины в задачи (asyncio.create_task); позволяет одним await ждать сразу несколько корутин/задач (asyncio.gather); предоставляет готовые awaitable‑объекты:

  • asyncio.sleep — ждать таймер;

  • сетевые клиенты и серверы для TCP/UDP/HTTP и т.п.;

  • примитивы синхронизации (Lock, Queue и др.).

import asyncio

async def fetch(i):
   print(f"start {i}")
   await asyncio.sleep(1)  # имитация запроса
   print(f"end {i}")
   return i
   
async def main():
   tasks = [asyncio.create_task(fetch(i)) for i in range(3)]
   results = await asyncio.gather(*tasks)
   print(results)
   
asyncio.run(main())

Что произойдёт:

  • все 3 fetch начнут выполняться почти одновременно;

  • каждый await asyncio.sleep(1) отдаст управление циклу;

  • цикл одновременно дождётся всех трёх sleep (через таймеры/Future) и по очереди продолжит каждую корутину;

общее время работы ≈ 1 секунда, а не 3.

asyncio.gather здесь создаёт один общий Future, который «готов», только когда завершились все три задачи, и await этого Future возвращает список результатов [1, 2, 3].

Оцени свой прогресс

Честно оцени своё понимание этого вопроса, чтобы мы могли построить твой учебный трек максимально эффективно.
Читать в блоге