Как event loop определяет, какую корутину прогрессировать при множестве корутин с разным временем ожидания в Python
Что проверяют
Понимаете ли вы, как устроено планирование в asyncio на самом деле. Ключевое: у цикла нет приоритетов и вытеснения, есть только очередь готовых задач и куча таймеров. Следом часто спрашивают, что будет, если внутри корутины вызвать time.sleep.
Как отвечать
Сначала поправьте формулировку: цикл не выбирает корутину, он выполняет то, что стало готовым. Каркас:
- Task: корутину оборачивают в
Task, который выполняет её по шагам до ближайшегоawaitна незавершённом future. Там задача отдаёт управление, а когда future завершится, её пробуждение ставится в очередь черезcall_soon; - две структуры: очередь готовых callback'ов
_ready(обычный FIFO) и мин-куча отложенных вызовов_scheduled, упорядоченная по времени срабатывания.asyncio.sleep(delay)это future плюсloop.call_later, то есть запись в куче; - одна итерация: цикл считает таймаут до ближайшего таймера (ноль, если готовые уже есть), опрашивает селектор (
epollна Linux,kqueueна macOS), переносит в очередь сработавшие события ввода-вывода и истёкшие таймеры и выполняет ровно те callback'и, что лежали в очереди на начало шага; - время:
loop.time()основан наtime.monotonic(), перевод системных часов на таймеры не влияет.
Отсюда ответ: при трёх sleep на 1, 2 и 3 секунды корутины просыпаются в порядке истечения таймеров, потому что куча отдаёт ближайший. Среди одновременно готовых порядок FIFO. await asyncio.sleep(0) таймер не заводит, это просто уступка управления.
Всё это кооперативно: пока корутина не дошла до await, остальные стоят. Блокирующие вызовы уносят в asyncio.to_thread, тяжёлые вычисления в пул процессов.
Где спотыкаются
Говорят, что цикл «приоритизирует задачи с меньшим ожиданием» или что asyncio.PriorityQueue влияет на планирование. Это обычная очередь для producer/consumer, к выбору корутин отношения не имеет.
Не отличают корутину от задачи: await coro() выполняет её последовательно, конкурентность появляется только через create_task, gather или TaskGroup.
Вызывают time.sleep или синхронный HTTP-клиент внутри корутины и удивляются, что «асинхронный» сервис обрабатывает запросы по одному.