При отмене asyncio-задачи, ожидающей результат функции в пуле потоков, прекращается ли уже выполняющаяся функция?
Нет. Отмена asyncio-задачи отменяет ожидание результата, но не прерывает функцию, которая уже выполняется в рабочем потоке. Она продолжит работу до естественного завершения, если сама функция не поддерживает кооперативную отмену.
asyncio создавался для эффективной работы с большим числом операций ввода-вывода в одном event loop. Однако существующий синхронный код часто нельзя сразу переписать в корутины, поэтому для него используются пулы потоков через asyncio.to_thread или run_in_executor.
Такое объединение асинхронного и синхронного кода требует различать отмену ожидания и остановку вычисления. Асинхронный код управляет задачами, но не получает безопасного универсального механизма принудительно оборвать произвольную функцию в другом потоке.
Если клиент отключился или истёк тайм-аут, сервер может отменить asyncio-задачу, ожидающую синхронную операцию. Ошибочно считать, что вместе с этим немедленно прекратится обращение к базе данных, файловая операция или внешний сетевой вызов в рабочем потоке.
Продолжившаяся функция может изменить данные, занять поток, удерживать соединение или выполнить побочный эффект уже после отмены HTTP-запроса. При массовых тайм-аутах это способно исчерпать пул потоков и создать дополнительную нагрузку.
При ожидании результата асинхронная задача связана с объектом будущего результата, который представляет работу в пуле. Вызов cancel() переводит asyncio-задачу в состояние отменённой и прерывает её ожидание. Если функция в потоке уже начала выполняться, отменить её через этот механизм нельзя.
Если работа ещё находится в очереди пула и не стартовала, отмена иногда может убрать её из очереди. Это зависит от состояния конкретного задания: после начала выполнения отмена будущего результата уже не останавливает функцию.
Минимальный пример:
Сначала будет отменено ожидание задачи, но примерно через две секунды рабочий поток всё равно напечатает сообщение. CancelledError сообщает вызывающей корутине об отмене, а не является сигналом принудительного завершения произвольного синхронного кода.
Надёжный вариант — проектировать функцию с кооперативной отменой. Ей передают потокобезопасный сигнал, например threading.Event, а функция периодически проверяет его между отдельными этапами работы. Если остановить функцию нельзя, следует учитывать её продолжение: ограничивать размер пула, делать операции идемпотентными и отделять жизненный цикл фоновой работы от жизненного цикла запроса.
Для CPU-bound работы поток также обычно не даёт настоящего параллелизма в CPython из-за GIL. В таких случаях рассматривают процессный пул, но отмена уже начавшейся работы в процессе тоже не становится автоматически безопасной: принудительное завершение процесса требует отдельной политики управления ресурсами и побочными эффектами.
В HTTP-сервисе обработчик запускает синхронную операцию в пуле потоков. Клиент разрывает соединение, поэтому обработчик отменяется, но операция продолжает выполняться и записывает результат в базу данных.
Вариант «считать отмену гарантированной остановкой» неверен: он приводит к неожиданным записям и исчерпанию пула. Вариант «ждать завершения всегда» сохраняет контроль над результатом, но может долго удерживать ресурсы запроса и не решает проблему отключившегося клиента.
Практичное решение — сделать операцию независимой от HTTP-запроса, использовать идемпотентный идентификатор операции и явно хранить её состояние. Если библиотека поддерживает тайм-ауты и остановку на уровне самой операции, их передают внутрь функции; если нет, задачу оставляют завершиться в ограниченном пуле. В результате отмена запроса не повреждает состояние, а фоновые операции не создают неограниченную конкуренцию.
1. Прекратит ли asyncio.wait_for работу функции в потоке после тайм-аута?
Нет. wait_for отменяет ожидаемую asyncio-задачу и сообщает вызывающему коду о тайм-ауте, но уже выполняющаяся функция в потоке продолжает работу. Тайм-аут ограничивает ожидание результата, а не обязательно саму синхронную операцию.
2. Можно ли остановить такую функцию, добавив проверку asyncio.CancelledError внутри неё?
Нет, если функция выполняется как обычный синхронный код в другом потоке. asyncio.CancelledError возникает в отменённой корутине, когда управление возвращается в неё, но не внедряется автоматически в произвольную функцию рабочего потока.
Для кооперативной остановки нужен отдельный потокобезопасный сигнал, который функция проверяет сама. Проверки должны находиться между безопасными этапами, поскольку остановка посередине критической операции может оставить частично изменённое состояние.
3. Что следует делать, если функция не поддерживает отмену, но результат после тайм-аута уже не нужен?
Нужно принять, что работа может завершиться позже, и управлять её последствиями явно. Обычно ограничивают размер пула, задают тайм-ауты на уровне используемой библиотеки, обеспечивают идемпотентность и не запускают неограниченное число фоновых операций.
Если продолжение работы недопустимо, простой cancel() недостаточен. Требуется механизм, поддерживаемый самой библиотекой, либо отдельный процесс, который можно завершить целиком с осознанием риска потери ресурсов и неконсистентного состояния.