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

Разберите последствия: что гарантирует withTaskCancellationHandler, если задача уже отменена до регистрации...

Разберите последствия: что гарантирует withTaskCancellationHandler, если задача уже отменена до регистрации обработчика?

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

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

Если задача уже отменена к моменту вызова withTaskCancellationHandler, обработчик onCancel будет вызван немедленно. Это не означает автоматической остановки operation: отмена в Swift кооперативная, поэтому операция всё ещё может начаться или продолжиться и должна самостоятельно проверять отмену.

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

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

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

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

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

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

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

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

withTaskCancellationHandler регистрирует обработчик, связанный с текущей задачей. Если задача уже отменена, Swift вызывает onCancel сразу; если отмена произойдёт после регистрации, обработчик будет вызван при обнаружении отмены. Рассчитывать на точный порядок между отменой, регистрацией обработчика и внутренними действиями операции нельзя.

Сам обработчик должен быть:

  • быстрым и синхронным;
  • безопасным при повторном обращении к состоянию отменяемого ресурса;
  • пригодным к вызову до фактического запуска операции;
  • независимым от предположения, что operation уже создала ресурс.

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

Операция также должна поддерживать кооперативную отмену. Многие системные async API сами выбрасывают CancellationError, но произвольная функция не обязана остановиться автоматически. Поэтому обработчику отмены и самой операции часто нужны согласованное состояние и проверка Task.checkCancellation() либо проверка результата отменяемого API.

Минимальная схема выглядит так:

let task = Task { await withTaskCancellationHandler(operation: { try? await Task.sleep(nanoseconds: 10_000_000_000) }, onCancel: { print("Запрос на освобождение ресурса") }) } task.cancel() await task.value

В этом примере cancel() только помечает задачу отменённой и вызывает обработчик. Task.sleep дополнительно реагирует на отмену и завершает ожидание, но такое поведение не следует автоматически из одного факта использования обработчика.

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

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

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

Первый вариант — просто проверять Task.isCancelled перед запуском запроса. Он дешёвый, но ненадёжен: отмена может произойти сразу после проверки.

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

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

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

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

  1. Достаточно ли вызова onCancel, чтобы операция гарантированно не выполнилась?

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

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

  1. Может ли обработчик отмены выполняться одновременно с operation?

Да, код должен быть рассчитан на такое взаимодействие. Нельзя без синхронизации изменять из onCancel обычное изменяемое состояние, которое одновременно читает или записывает operation.

Для согласования используют изолированный actor, блокировку или другой безопасный примитив, соответствующий конкретному ресурсу. Важно также, чтобы отмена была идемпотентной: повторная попытка закрыть или отменить уже отменённый ресурс не должна приводить к повреждению состояния.

  1. Что делать, если отмена произошла до создания ресурса?

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

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