В чем отличие корутины от таски и когда начинается выполнение кода корутины и таски
Что проверяют
Понимаете ли вы, что вызов корутинной функции ничего не запускает, и чем ожидание корутины отличается от планирования задачи. Следом часто дают код с двумя await подряд и спрашивают, почему он выполняется последовательно, а не конкурентно.
Как отвечать
Разведите три понятия:
- корутинная функция: объявлена через
async def. Её вызов не выполняет ни строчки тела, а возвращает объект корутины; - корутина: объект, который выполняется, только когда его ждут через
awaitили отдают циклу событий. Если про него забыть, Python выдастRuntimeWarning: coroutine ... was never awaited; - задача (
asyncio.Task): обёртка, которая регистрирует корутину в цикле событий.TaskнаследуетFuture: её можно отменить черезcancel(), дождаться и забрать результат.
Теперь про момент старта:
await coro(): тело начинает выполняться сразу, внутри текущей задачи, и идёт синхронно до первой настоящей приостановки. Вызывающий код ждёт завершения;asyncio.create_task(coro()): задача только ставится в очередь цикла. Её код начнёт выполняться, когда текущая корутина отдаст управление, то есть на ближайшемawait, который действительно приостанавливает. Если послеcreate_taskидёт длинный синхронный код, задача всё это время стоит.
Отсюда практика: чтобы запустить работу конкурентно, создают задачи или используют asyncio.gather и asyncio.TaskGroup (с Python 3.11), а не пишут await подряд. С Python 3.12 есть asyncio.eager_task_factory: с ней задача начинает выполняться синхронно прямо при создании, до первой приостановки.
Где спотыкаются
Говорят, что задача стартует в момент create_task. По умолчанию это не так, и на этом строят задачки про порядок вывода print.
Не сохраняют ссылку на задачу. Цикл событий держит задачи через слабые ссылки, и задачу без других ссылок сборщик мусора может убрать посреди работы. Документация прямо просит сохранять результат create_task.
Пишут await a() и следом await b() и ждут параллельности. Корутины выполнятся по очереди, а блокирующий вызов вроде time.sleep внутри любой из них остановит весь цикл.