Как asyncio понимает, что задача завершилась и можно продолжать выполнение после await
Что проверяют
Знаете ли вы механику под await, а не только синтаксис. Интервьюер хочет услышать про Future, колбэки и Task, а следом спрашивает, чем await coro() отличается от await asyncio.create_task(coro()) и что будет от time.sleep внутри корутины.
Как отвечать
Держите в голове цепочку из четырёх звеньев:
- корутина: вызов
async defфункции только создаёт объект, код не выполняется, пока его не ожидают черезawaitили не обернут вTask; - Task: двигает корутину шагами, вызывая
coro.send(None). СамTaskявляется подклассомFuture; - Future: объект с состоянием и списком колбэков. Его
__await__отдаёт наружу сам себя черезyield, и этот объект доходит по цепочке доTask; - цикл событий: получив незавершённый
Future,Taskвешает на него колбэк черезadd_done_callbackи возвращает управление циклу.
Возобновление происходит так. Кто-то вызывает fut.set_result(...): колбэк селектора (epoll или kqueue сообщил, что сокет готов), таймер или другая задача. Future планирует свои колбэки через loop.call_soon, на следующей итерации цикл будит Task, тот снова делает send, и корутина продолжает с места await уже с результатом. Исключение или отмена передаются через coro.throw.
Важная деталь: await другой корутины переключения не вызывает, это вызов внутри той же задачи, как yield from. Переключение случается только там, где по цепочке кто-то отдаёт незавершённый Future или делает голый yield, как asyncio.sleep(0).
Где спотыкаются
Говорят, что вызов корутины ставит её в очередь цикла. Не ставит: без await или create_task она не запустится, и Python выдаст предупреждение «coroutine was never awaited».
Считают, что цикл сам опрашивает задачи на завершённость. Опроса нет, всё держится на колбэках, которые Future вызывает при set_result.
Путают asyncio с многопоточностью. Всё идёт в одном потоке, поэтому блокирующий вызов останавливает весь цикл. Такие вызовы выносят через asyncio.to_thread или run_in_executor.