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

Задачу asyncio отменяют во время ожидания ввода вывода. В какой момент и каким механизмом корутина узнаёт о...

Задачу asyncio отменяют во время ожидания ввода-вывода. В какой момент и каким механизмом корутина узнаёт об отмене?

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

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

Отмена задачи в asyncio является кооперативным запросом, а не принудительным убийством корутины. Если задача ожидает в точке приостановки, например во время await, при её возобновлении в корутину выбрасывается исключение asyncio.CancelledError; корутина может выполнить очистку, пробросить исключение дальше или ошибочно подавить отмену.

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

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

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

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

Вызов Task.cancel() не означает, что корутина немедленно прекратилась. Он лишь запрашивает отмену; если корутина долго выполняет синхронный CPU-bound код и не достигает точки приостановки, обработка запроса задержится.

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

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

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

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

import asyncio async def worker(): try: await asyncio.sleep(10) except asyncio.CancelledError: print("Очистка") raise async def main(): task = asyncio.create_task(worker()) await asyncio.sleep(0) task.cancel() try: await task except asyncio.CancelledError: print("Задача отменена") asyncio.run(main())

Здесь cancel() только устанавливает запрос, а await task получает результат этого запроса после того, как worker обработает CancelledError. Блок finally или обработчик CancelledError подходит для закрытия соединений, освобождения блокировок и отмены связанных операций.

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

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

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

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

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

Выбран третий вариант: корутина закрывает временные ресурсы в finally, отменяет связанные дочерние задачи и не подавляет CancelledError без специальной причины. В результате запросы прекращаются вместе с жизненным циклом клиента, а пул соединений не удерживается лишними операциями.

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

  1. Дополнительный вопрос: Может ли Task.cancel() прервать корутину, которая выполняет бесконечный синхронный цикл?

Ответ: Нет, не своевременно. Запрос отмены обрабатывается только когда корутина возвращает управление циклу событий, обычно через await; синхронный бесконечный цикл блокирует поток и не даёт исключению быть доставленным. Такую работу нужно перепроектировать, вынести из event loop или выполнять в подходящем исполнителе.

  1. Дополнительный вопрос: Что произойдёт, если корутина перехватит CancelledError и завершится обычным результатом?

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

  1. Дополнительный вопрос: Зачем выполнять очистку в finally, если CancelledError уже можно перехватить?

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