Объясните механизм: почему объект корутины сам по себе не начинает выполнение?
Вызов функции, объявленной через async def, создаёт объект корутины, но не запускает её тело. Выполнение начинается только после передачи этого объекта в await, asyncio.create_task или другой механизм, который планирует корутину.
Если объект корутины уничтожить, не выполнив его, Python обычно выдаёт предупреждение RuntimeWarning: coroutine was never awaited. Поэтому простого вызова асинхронной функции недостаточно: результат нужно явно ожидать или запланировать.
Асинхронные корутины появились как способ описывать операции, которые могут добровольно приостанавливаться, не блокируя поток. В синхронной функции вызов сразу выполняет тело, а async def должен позволять отделить создание вычисления от его планирования.
Такое разделение необходимо для работы планировщика asyncio: он должен сам решать, какие корутины поставить на выполнение, когда переключить их и как связать их с ожиданием ввода-вывода.
Ошибка возникает, когда разработчик вызывает асинхронную функцию, ожидая немедленного выполнения её побочных эффектов: записи в базу, отправки запроса или изменения состояния. Фактически создаётся только объект-контейнер с описанием будущего выполнения, а код внутри функции ещё не запускался.
Последствия могут быть разными: операция не произойдёт вовсе, появится предупреждение при сборке мусора, а исключение из корутины не возникнет в ожидаемый момент. При массовом создании таких объектов приложение также может удерживать ненужные ресурсы до их уничтожения.
Вызов async def возвращает объект корутины. В тело функции выполнение переходит при обработке этого объекта оператором await или после постановки его в планировщик через asyncio.create_task.
await означает: текущая корутина передаёт управление вызывающей корутине или event loop и ждёт результата. Task оборачивает корутину, регистрирует её в event loop и позволяет ей выполняться независимо от непосредственного места создания, пока цикл событий продолжает работу.
В первом случае корутина выполняется как часть текущего ожидания. Во втором сначала создаётся отдельная задача, после чего её результат явно ожидается. Само выражение load_value() без await или create_task не напечатает ни одной строки из тела функции.
Важно отличать корутину от Task. Корутина — это ещё не запланированное выполнение, а Task — управляемая event loop задача, у которой можно получить результат, дождаться завершения или запросить отмену. Создание Task обычно требует работающего event loop.
Нельзя бездумно создавать корутины и откладывать их выполнение: у них могут накапливаться связанные с операцией данные, а ошибки проявятся позже или будут скрыты предупреждением. Если выполнение нужно гарантировать, корутину следует сразу ожидать либо сохранить Task и корректно дождаться его завершения.
В обработчике пакетного импорта разработчик для каждого элемента вызывает асинхронную функцию отправки уведомления, но не использует её результат. Импорт завершается успешно, однако уведомления не отправляются, а в журналах появляются предупреждения о неожидаемых корутинах.
Рассматривались два варианта:
Выбран второй вариант с явным ожиданием группы задач и ограничением параллелизма. Такой подход сохраняет гарантии завершения и не создаёт неограниченную нагрузку на внешний сервис. Результат: уведомления действительно отправляются до завершения импорта, а ошибки становятся доступными вызывающему коду.
1. Что произойдёт, если корутину передать в asyncio.gather, но не создать для неё Task заранее?
asyncio.gather принимает awaitable-объекты, включая корутины, и планирует переданные корутины как задачи внутри текущего event loop. Поэтому предварительный вызов create_task не обязателен. Важно лишь дождаться самого gather; если не ожидать и его, выполнение группы не будет гарантировано.
2. Всегда ли потеря ссылки на Task означает немедленную остановку его выполнения?
Нет, Task обычно продолжает выполняться, пока event loop удерживает необходимые ссылки и задача может продвигаться. Однако документация asyncio предупреждает, что event loop хранит задачи слабыми ссылками, поэтому для фоновой задачи следует сохранять сильную ссылку до её завершения.
Практически это означает, что фоновые задачи нужно складывать в коллекцию, удаляя их после завершения, либо использовать собственный механизм управления жизненным циклом. Иначе задача может быть уничтожена до окончания работы, особенно если больше нигде не сохраняется.
3. Что произойдёт с исключением, если Task запущен, но его результат никто не ожидает?
Исключение сохранится внутри Task, но вызывающий код не получит его через await. При уничтожении Task asyncio может вывести предупреждение о том, что исключение никогда не было извлечено.
Надёжная программа должна либо ожидать Task и обрабатывать исключение, либо установить явный механизм наблюдения за завершением фоновой задачи. Простое создание Task не означает автоматической передачи его ошибок в текущую корутину.