Программирование SwiftКонкурентностьРазработчик приложений на Swift

В async функции, выполняющей вычислительный цикл без await, как корректно обнаружить отмену задачи?

В async-функции, выполняющей вычислительный цикл без await, как корректно обнаружить отмену задачи?

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

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

Отмена в Swift кооперативная: задача не прерывается принудительно. В вычислительном цикле нужно периодически проверять состояние отмены через Task.checkCancellation() — он выбрасывает CancellationError, — либо через Task.isCancelled, если требуется самостоятельно завершить работу без исключения.

Проверка должна выполняться в самой длительной операции или цикле. Одного вызова cancel() недостаточно: он только устанавливает признак отмены у целевой задачи.

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

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

Вместо этого Swift отделяет запрос отмены от реакции на него. Код сам выбирает безопасную точку остановки и может корректно освободить ресурсы или сохранить частичный результат.

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

Представим CPU-затратный цикл, который не вызывает await: проверка отмены не произойдёт автоматически, потому что выполнение не приостанавливается и управление не передаётся планировщику. Такая задача продолжит занимать вычислительные ресурсы даже после запроса отмены.

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

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

Task.checkCancellation() проверяет состояние текущей задачи и при отмене выбрасывает CancellationError. Поэтому вызывающая async-функция должна быть throws либо обработать ошибку внутри себя.

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

func process(_ items: [Item]) async throws -> Result { var result = Result() for item in items { try Task.checkCancellation() result.add(processItem(item)) } return result }

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

Многие отменяемые async-операции сами реагируют на отмену, но это не отменяет обязанности учитывать её в собственном CPU-bound коде. Для больших вычислений полезны пакетная обработка, периодические проверки и корректное освобождение ресурсов.

Важно отличать отмену от ошибки: CancellationError — соглашение о прекращении работы, а не механизм принудительного уничтожения задачи. После перехвата ошибки код может вернуть частичный результат, выполнить очистку или повторно пробросить её — в зависимости от контракта функции.

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

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

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

Оптимальное решение — обрабатывать изображения небольшими партиями и вызывать Task.checkCancellation() перед каждой следующей единицей работы. Это сохраняет отзывчивость, позволяет корректно прервать вычисление и не требует небезопасного принудительного завершения.

Если частичный результат допустим, вместо выбрасывания ошибки можно проверить Task.isCancelled, выйти из цикла и вернуть уже готовые превью. Выбор между двумя вариантами определяется API: для операции, которая должна завершиться полностью или неуспешно, уместнее checkCancellation(); для потоковой или пакетной обработки — явный возврат частичного результата.

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

  1. Достаточно ли вызвать Task.yield() для обнаружения отмены?

Нет. Task.yield() может добровольно передать управление другим задачам, но сам по себе не обязан выбрасывать CancellationError и не заменяет явную проверку. Он улучшает планирование и отзывчивость, тогда как Task.checkCancellation() проверяет контракт отмены.

  1. Можно ли считать Task.isCancelled == true сигналом немедленного завершения текущей инструкции?

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

  1. Что произойдёт, если обработать CancellationError и продолжить тот же цикл?

Задача останется отменённой: обработка исключения не сбрасывает состояние отмены. Если продолжение работы не предусмотрено контрактом, после очистки нужно вернуть управление или повторно выбросить ошибку. Игнорирование ошибки может привести к бесполезной работе после ухода потребителя и нарушить ожидаемую семантику отмены.