Как event loop управляет тасками
Что проверяют
Понимаете ли вы asyncio изнутри: что такое задача, когда она реально начинает выполняться и почему один блокирующий вызов останавливает всё. Следом обычно просят объяснить разницу между корутиной и Task или спрашивают, чем gather отличается от TaskGroup.
Как отвечать
Event loop однопоточный и работает по принципу кооперативной многозадачности: задачи сами отдают управление на каждом await, который не может завершиться сразу. Параллельности нет, есть конкурентность.
Task: обёртка над корутиной, наследникFuture.asyncio.create_task()не выполняет корутину сразу, а ставит её первый шаг в очередь готовых колбэков;- шаг задачи: цикл вызывает
coro.send(None), и корутина выполняется доawaitна незавершённомFuture. Задача вешает на этотFutureколбэк пробуждения и возвращает управление; - итерация цикла: считает таймаут до ближайшего таймера (
call_later,asyncio.sleep), ждёт готовности сокетов через селектор (epoll в Linux, kqueue в macOS), переносит сработавшие I/O-события и таймеры в очередь готовых и выполняет всё, что в ней лежит; - пробуждение: когда
Futureзавершился, его колбэк черезcall_soonснова ставит шаг задачи в очередь.
asyncio.run() создаёт цикл, выполняет главную корутину, по её завершении отменяет оставшиеся задачи и закрывает цикл. С Python 3.11 есть asyncio.TaskGroup: при ошибке одной задачи остальные отменяются, а исключения собираются в ExceptionGroup. Блокирующий код выносят в asyncio.to_thread() или run_in_executor.
Где спотыкаются
Вызывают синхронную функцию внутри корутины: time.sleep, requests.get, тяжёлый расчёт. Цикл стоит, все задачи ждут, а метрики показывают необъяснимые задержки.
Не сохраняют ссылку на результат create_task. Цикл держит задачи через слабые ссылки, и задачу может собрать сборщик мусора до завершения, документация прямо об этом предупреждает.
Говорят, что gather выполняет корутины параллельно, как потоки. Это переключение в одном потоке, и нагрузку на CPU оно не ускоряет.