Программирование PythonКонкурентность и asyncioPython-разработчик серверных приложений

Сравните прямой await корутины и передачу её в asyncio.create task: в чём главное различие их запуска?

Сравните прямой await корутины и передачу её в asyncio.create_task: в чём главное различие их запуска?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

await запускает корутину в текущем месте и приостанавливает текущую корутину до её завершения. asyncio.create_task регистрирует корутину как отдельную задачу в цикле событий и сразу возвращает объект задачи, поэтому вызывающая корутина может продолжить работу.

Оба варианта выполняются в том же потоке и не создают отдельный поток автоматически. Параллелизм здесь кооперативный: другие задачи получают управление только в точках, где текущая задача уступает его, обычно через await.

Исторический контекст

Асинхронная модель появилась как способ обслуживать большое количество операций ввода-вывода без создания отдельного потока на каждое ожидание. Цикл событий хранит готовые к выполнению задачи и переключает управление между ними, когда текущая задача временно не может продолжать работу.

В asyncio корутина сама по себе описывает асинхронную работу, но не обязательно уже запланирована на выполнение. Объект Task связывает корутину с циклом событий и позволяет управлять её жизненным циклом: ожидать завершения, отменять задачу и получать её результат.

Постановка проблемы

Если последовательно сделать два await, вторая операция начнётся только после завершения первой. Это корректно для зависимых шагов, но неоптимально, если операции независимы и большую часть времени ждут сеть, диск или другой асинхронный ресурс.

Без понимания разницы между await и create_task разработчик может случайно сериализовать независимые операции или, наоборот, запустить фоновую задачу без контроля её результата. Во втором случае возможны необработанные исключения, преждевременное завершение приложения или потеря задачи при отсутствии ссылки на неё.

Подробное решение

При await some_coroutine() вызывающая корутина передаёт управление вложенной корутине и не продолжает выполнение, пока вложенная корутина не завершится или не завершится с исключением. При этом цикл событий всё ещё может выполнять другие задачи, если вложенная корутина сама приостанавливается на асинхронном ожидании.

asyncio.create_task(some_coroutine()) создаёт и планирует Task. Вызов возвращает управление сразу, а новая задача начнёт выполняться, когда цикл событий получит возможность её запустить. Чтобы дождаться результата, задачу затем обычно передают в await или в asyncio.gather.

import asyncio async def load(name, delay): await asyncio.sleep(delay) return name async def main(): first = asyncio.create_task(load("A", 1)) second = asyncio.create_task(load("B", 1)) result = await asyncio.gather(first, second) print(result) asyncio.run(main())

В примере обе операции запланированы до ожидания результата, поэтому их асинхронные ожидания перекрываются. Если вместо этого последовательно выполнить два await load(...), общее время ожидания обычно будет близко к сумме задержек, а не к максимальной задержке.

create_task не превращает синхронную или CPU-bound функцию в неблокирующую. Если задача надолго выполняет вычисления без точек уступки, она блокирует цикл событий и мешает другим задачам. Для блокирующей работы применяют отдельный поток или процесс через подходящий механизм выполнения, учитывая ограничения GIL, стоимость переключения и безопасность доступа к данным.

Запущенную задачу следует контролировать: сохранить её ссылку, дождаться результата либо явно обработать исключение и отмену. Для независимого набора операций удобнее asyncio.gather, поскольку он выражает групповое ожидание и позволяет определить политику обработки исключений.

Ситуация из практики

HTTP-обработчику нужно получить профиль пользователя и список рекомендаций. Эти запросы независимы, но каждый ждёт удалённый сервис.

Последовательный вариант проще: сначала ожидается профиль, затем рекомендации. Его плюс — очевидный порядок и простая обработка зависимостей; минус — задержки складываются, поэтому ответ медленнее.

Вариант с двумя create_task запускает операции до ожидания результатов. Он уменьшает задержку примерно до времени более медленного запроса, но требует явно решить, что делать, если одна задача завершилась ошибкой или была отменена.

Выбран вариант с созданием обеих задач и групповым ожиданием через asyncio.gather, потому что операции независимы, а обработчику нужен согласованный результат обеих. Если профиль обязателен, а рекомендации необязательны, вместо безусловного общего ожидания применяют раздельную обработку ошибок и отмену ненужной задачи при отказе критического запроса.

Что кандидаты часто упускают

  1. Всегда ли create_task начинает выполняться немедленно?

Нет. Задача становится запланированной, но фактически получает управление только при следующем подходящем шаге цикла событий. Текущая корутина может продолжить синхронный код до ближайшей точки уступки, поэтому создание задачи не означает немедленного вытеснения текущего выполнения.

  1. Что произойдёт с исключением задачи, если её не ожидать?

Исключение сохраняется внутри объекта Task. Если результат задачи не получить через await, gather или другой механизм наблюдения, цикл событий обычно сообщит о необработанном исключении при уничтожении задачи, но вызывающая логика не получит его как обычное исключение в момент возникновения.

Практический вывод: фоновые задачи должны иметь явную стратегию наблюдения — обработчик результата, ожидание в жизненном цикле приложения или централизованный механизм управления фоновыми задачами.

  1. Можно ли использовать create_task для корутины, которая никогда не уступает управление?

Технически задачу создать можно, но пользы для конкурентности не будет. Пока такая корутина выполняет CPU-bound код без await, цикл событий не сможет запустить другие задачи, обслужить таймеры или своевременно обработать сетевые события.

Вычисления следует вынести из цикла событий в поток или процесс, если это соответствует характеру нагрузки. При этом поток в CPython не гарантирует ускорение CPU-bound Python-кода из-за GIL, тогда как процессы обеспечивают отдельные интерпретаторы, но требуют сериализации данных и несут дополнительные накладные расходы.