При истечении тайм-аута asyncio.wait_for что происходит с ожидаемой корутиной?
При истечении тайм-аута asyncio.wait_for обычно отменяет ожидаемую корутину или задачу, дожидается обработки этой отмены и затем возбуждает TimeoutError у вызывающего кода. Поэтому тайм-аут ограничивает не только ожидание результата, но и инициирует завершение вложенной операции.
Если нужно прекратить ожидание, но позволить самой операции продолжиться, её защищают через asyncio.shield.
Асинхронные сервисы часто должны ограничивать задержку сетевых запросов, обращения к базам данных и других внешних операций. Один зависший вызов без тайм-аута способен удерживать задачу, соединение и связанные ресурсы неопределённо долго.
asyncio.wait_for появился как высокоуровневый способ связать ожидание результата с ограничением времени. При этом тайм-аут в asyncio реализован через уже существующий механизм кооперативной отмены, а не через принудительное убийство выполняющегося кода.
Предположим, обработчик HTTP-запроса ждёт внешний сервис не более двух секунд. Если по истечении этого времени только прекратить ждать результат, внешняя операция всё равно может продолжиться и расходовать соединение, память или слот ограниченного пула.
Если же бездумно отменять вложенную задачу, можно прервать важную операцию: например, запись в журнал, подтверждение платежа или обновление состояния. Ошибка проектирования здесь состоит в смешении двух решений: ограничения времени ожидания и отмены самой работы.
Когда wait_for достигает тайм-аута, он вызывает отмену ожидаемого объекта. Для корутины это обычно означает создание внутренней задачи и отправку ей CancelledError в ближайшей точке, где она передаёт управление event loop.
Отмена не прерывает синхронный Python-код посреди инструкции. Корутина должна получить управление и корректно завершиться, освободив ресурсы в блоках finally. Поэтому фактическое возвращение из wait_for может произойти позже указанного тайм-аута: механизм ждёт завершения обработки отмены.
Обычный вариант выглядит так:
Здесь через две секунды вложенная корутина получает отмену, а main обычно получает TimeoutError. Если вложенная корутина перехватывает CancelledError и продолжает работу, отмена может быть задержана; подавлять отмену без крайней необходимости опасно.
Если требуется ограничить только ожидание, применяют shield:
В этом случае wait_for отменяет защищённое ожидание, но не саму task. Однако задача продолжает занимать ресурсы, поэтому её нужно сохранить, затем дождаться, отменить или передать специализированному обработчику.
Важное ограничение: wait_for не способен остановить уже выполняющийся блокирующий синхронный вызов. Если корутина выполняет такой вызов без передачи его в поток или процесс, event loop не получает управление и тайм-аут своевременно не срабатывает.
В HTTP-сервисе запрос к рекомендательной системе должен укладываться в 300 миллисекунд. Команда сначала обернула вызов в wait_for, но после тайм-аута обнаружила продолжающиеся фоновые запросы и рост числа занятых соединений.
Рассматривались два варианта. Без shield тайм-аут автоматически отменял запрос к рекомендательной системе и освобождал ресурсы быстрее, но отменял и результат, который уже мог быть нужен для кэширования. С shield основной HTTP-ответ можно было вернуть вовремя, а внешний запрос продолжить, но требовались контроль жизненного цикла задачи, ограничение числа фоновых операций и обработка её исключений.
Выбрали обычную отмену через wait_for, потому что результат после закрытия HTTP-запроса не имел ценности. Для действительно обязательных фоновых операций применили отдельные задачи с явным владельцем, ограничением очереди и обработчиком завершения. Это предотвратило накопление невидимых задач и сохранило предсказуемое потребление ресурсов.
1. Вопрос: Прекращает ли тайм-аут выполнение синхронной функции, уже запущенной внутри корутины?
Ответ: Нет. Отмена действует на корутину, но не может безопасно прервать произвольный синхронный Python-код, который уже удерживает поток event loop. Такой код сначала должен завершиться сам, поэтому тайм-аут может не сработать вовремя. Для блокирующей работы используют поток или процесс, понимая, что отмена задачи обычно не останавливает уже выполняющуюся функцию в пуле.
2. Вопрос: Чем отличается wait_for(coro, timeout) от wait_for(shield(task), timeout)?
Ответ: В первом случае тайм-аут распространяется на ожидаемую корутину: wait_for пытается отменить её. Во втором случае отменяется оболочка ожидания, а защищённая задача продолжает выполняться. shield не делает операцию неотменяемой вообще: прямой вызов task.cancel() или отмена владельца самой задачи всё ещё может её завершить.
3. Вопрос: Почему фактическое время возврата после тайм-аута иногда больше заданного лимита?
Ответ: Современный wait_for не просто отправляет отмену, а дожидается, пока вложенная задача завершит обработку отмены. Если в finally выполняется медленное освобождение ресурса или корутина подавляет CancelledError, возврат задерживается. Поэтому тайм-аут следует понимать как момент начала отмены, а не как гарантию жёсткого убийства работы в точную миллисекунду.