Event loop, async/await и корутины
Тысячи задач в одном потоке без единой блокировки: как работает asyncio.
Event loop (цикл событий) — планировщик в одном потоке, который запускает множество корутин, переключаясь между ними в точках ожидания; корутина — функция, умеющая приостанавливаться и возобновляться.
asyncio решает проблему I/O-конкурентности иначе, чем потоки. Вместо того чтобы ОС вытесняла потоки, корутины сами добровольно уступают управление в точке await. Это называется кооперативной многозадачностью, и работает она в одном потоке без GIL-борьбы и без блокировок.
async и await
async def объявляет корутину. await — это точка «здесь я могу подождать; пока жду, отдаю управление циклу событий, пусть он займётся другими корутинами».
import asyncio
async def fetch(name, delay):
print(f"{name}: старт")
await asyncio.sleep(delay) # уступаю циклу на время ожидания
print(f"{name}: готово")
return name
async def main():
# запускаем три корутины «внахлёст»
await asyncio.gather(
fetch("A", 2), fetch("B", 1), fetch("C", 3)
)
asyncio.run(main())Этот код использует реальный event loop asyncio, поэтому в браузере мы его не запускаем. Но саму идею кооперативного чередования легко смоделировать.
Модель кооперативного чередования
# корутины как список «шагов до await»; цикл крутит их по очереди
tasks = {"A": ["старт", "ждёт", "готово"],
"B": ["старт", "готово"],
"C": ["старт", "ждёт", "готово"]}
step = 0
while any(step < len(s) for s in tasks.values()):
for name, steps in tasks.items():
if step < len(steps):
print(f"loop tick {step}: {name} -> {steps[step]}")
step += 1Вывод:
loop tick 0: A -> старт loop tick 0: B -> старт loop tick 0: C -> старт loop tick 1: A -> ждёт loop tick 1: B -> готово loop tick 1: C -> ждёт loop tick 2: A -> готово loop tick 2: C -> готово
Как работает под капотом
Event loop хранит очередь готовых к выполнению корутин. Дойдя до await на чём-то ещё не готовом (например, сетевой ответ не пришёл), корутина замораживается: цикл запоминает, на чём она остановилась, и берёт следующую готовую. Когда ожидаемое событие происходит, корутину возвращают в очередь. Поскольку всё в одном потоке и переключение только в точках await, гонок на общих данных между await почти не бывает — это большое упрощение. Но есть и обратная сторона: одна корутина с тяжёлым счётом без await «застопорит» весь цикл.
Частые ошибки
- Звать блокирующую функцию внутри корутины. Обычный
time.sleep()или синхронный запрос заморозит весь loop — нужны асинхронные аналоги (asyncio.sleep). - Считать loop магически параллельным. Это один поток: CPU-bound код его блокирует, асинхронность помогает только с ожиданием.
- Забыть
await. Корутина безawaitне запустится — вы получите объект корутины, а не результат.
Итог
- asyncio — кооперативная многозадачность в одном потоке.
- Корутины уступают управление добровольно в точках
await. - Подходит для I/O: тысячи ожиданий без потоков и блокировок.
- Блокирующий или CPU-bound код останавливает весь event loop.