Как работает цикл событий в asyncio и в каком порядке выполняются таски
Что проверяют
Понимаете ли вы, что asyncio однопоточный и что задача не выполняется в момент создания. Типичное продолжение: интервьюер показывает код с create_task, gather и sleep(0) и просит предсказать порядок вывода.
Как отвечать
Цикл событий работает в одном потоке и на каждой итерации делает три вещи:
- ожидание I/O: через селектор (epoll, kqueue) узнаёт, какие сокеты готовы, с таймаутом до ближайшего таймера;
- таймеры: переносит из кучи отложенных вызовов (
call_later,asyncio.sleep) те, чей срок наступил, в очередь готовых; - очередь готовых: по порядку FIFO выполняет колбэки, которые стояли в ней на начало итерации.
Задача (Task) это обёртка над корутиной. asyncio.create_task(coro) через call_soon ставит в очередь первый шаг корутины, но не запускает его. Корутина выполняется, пока не дойдёт до await на том, что действительно приостанавливается: future, сокет, sleep. Тогда управление возвращается в цикл, и очередь переходит к следующей готовой задаче.
Отсюда правила порядка:
- создание подряд: задачи стартуют в порядке создания, но только когда текущая корутина уступит управление;
await asyncio.sleep(0): явная уступка без ожидания;await some_coro(): без задачи корутина выполняется сразу внутри текущей, без переключения;gather: результаты в порядке аргументов, а не завершения. Порядок завершения даётasyncio.as_completed.
Для современного кода добавьте asyncio.TaskGroup из Python 3.11: при ошибке одной задачи она отменяет соседние. С Python 3.12 есть asyncio.eager_task_factory: задача начинает выполняться синхронно ещё при создании, до первой приостановки, и порядок меняется.
Где спотыкаются
Вызывают внутри корутины блокирующий код: time.sleep, requests, тяжёлый расчёт. Цикл встаёт целиком, все задачи ждут. Такие вызовы уносят в asyncio.to_thread или run_in_executor.
Не хранят ссылку на результат create_task. Цикл держит задачи слабыми ссылками, и незавершённую задачу может забрать сборщик мусора.
Путают конкурентность с параллельностью: asyncio не использует несколько ядер, он переключается между задачами на ожиданиях ввода-вывода.