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

Вам дан код с ограничением времени ожидания. Как asyncio.wait обработает задачу, не завершившуюся к моменту...

Вам дан код с ограничением времени ожидания. Как asyncio.wait обработает задачу, не завершившуюся к моменту тайм-аута?

import asyncio

async def job():
    await asyncio.sleep(5)
    return 42

async def main():
    task = asyncio.create_task(job())
    done, pending = await asyncio.wait({task}, timeout=0.1)
    print(task in done, task in pending, task.cancelled())

asyncio.run(main())
Проходите собеседования с ИИ помощником Hintsage

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

После истечения timeout задача попадёт в pending, а asyncio.wait не отменит её автоматически. В данном примере будут напечатаны False True False: задача продолжит выполняться, если event loop продолжит работу.

asyncio.wait только возвращает два множества задач: завершившихся и ещё ожидающих. Решение об отмене или дальнейшем ожидании незавершённых задач остаётся за вызывающим кодом.

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

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

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

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

Если разработчик предполагает, что тайм-аут asyncio.wait автоматически остановит незавершённые задачи, фоновые операции могут продолжить работу после завершения внешнего запроса. Это приводит к лишней нагрузке, побочным эффектам и ошибкам вида "задача уничтожена при завершении работы цикла".

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

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

asyncio.wait принимает набор объектов, ожидающих завершения, и возвращает пару (done, pending). При timeout он прекращает собственное ожидание, когда истекает срок, но не вызывает cancel() для задач из pending.

В примере task ещё спит, поэтому она находится в pending. Вызов task.cancelled() возвращает False: отмена не запрашивалась. Если после этого event loop продолжит работу, задача проснётся через пять секунд и завершится обычным результатом.

Если задачи нужно остановить, отмену следует выполнить явно и затем дождаться их завершения:

import asyncio async def main(): task = asyncio.create_task(asyncio.sleep(5)) done, pending = await asyncio.wait({task}, timeout=0.1) for task in pending: task.cancel() await asyncio.gather(*pending, return_exceptions=True) asyncio.run(main())

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

asyncio.wait_for ведёт себя иначе: при истечении тайм-аута он по умолчанию отменяет ожидаемый объект и ждёт обработки этой отмены. Поэтому выбор между wait и wait_for выражает разные намерения: wait возвращает состояние набора задач, а wait_for ограничивает ожидание конкретного объекта с политикой отмены.

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

Сервис запускает несколько независимых запросов к поставщикам и хочет собрать ответы, доступные за 300 миллисекунд. Вариант с последовательным await прост, но медленный: задержка одного поставщика блокирует получение результатов остальных. Вариант с wait_for вокруг каждой операции автоматически отменяет отдельные запросы по их тайм-аутам, но не даёт одного общего решения для всей группы.

asyncio.wait с общим тайм-аутом позволяет сразу получить готовые результаты и отдельно решить судьбу оставшихся задач. Если внешние запросы больше не нужны после ответа клиенту, выбранное решение — явно отменить pending и дождаться их завершения с return_exceptions=True; это предотвращает утечки фоновой работы и позволяет корректно выполнить очистку.

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

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

  1. Можно ли вызвать task.cancel() только для задач из pending?

Да, обычно именно так и делают, если отмена требуется. Но после отправки запроса на отмену следует дождаться задач через await asyncio.gather(*pending, return_exceptions=True), чтобы дать им выполнить блоки finally и не получить необработанные последствия отмены.

  1. Что произойдёт с незавершённой задачей, если main сразу завершится?

При завершении asyncio.run выполняет завершающие действия для event loop и отменяет оставшиеся задачи. Это не свойство asyncio.wait, а поведение жизненного цикла asyncio.run. Поэтому в коротком примере без дополнительного ожидания задача фактически не получит возможность дожить до пяти секунд.

  1. Гарантирует ли попадание задачи в done, что её результат успешно получен?

Нет. В done находятся задачи, завершившиеся успешно, с исключением или отменой. Для различения состояний нужно проверить task.cancelled(), а затем вызвать task.result() или обработать исключение; вызов result() для задачи с ошибкой повторно выбросит это исключение.