Вся асинхронность крутится вокруг одного объекта — event loop (событийный цикл). Он делает две вещи:
запускает
asyncdef‑функции (корутины);переключается между ними, когда они чего‑то ждут (сетевой ответ, таймер и т.п.), чтобы в это время выполнялся другой код.
Важно: это всё происходит в одном потоке и с тем же 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) делает примерно следующее:
Хранит список задач (Task).
Берёт задачу, продвигает её корутину до ближайшего
await.Если корутина упёрлась в
await asyncio.sleep(), ожидание сети и т.п., задача помечается как «ждущая», и цикл запускает следующую задачу.Внутри у цикла есть ожидание событий ОС (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].