На каком исполнителе выполняется обработчик отмены в withTaskCancellationHandler?
withTaskCancellationHandler не гарантирует выполнение обработчика отмены на том же исполнителе, потоке или акторе, где выполняется основная операция. Обработчик должен рассматриваться как синхронный Sendable-код, способный выполняться конкурентно с операцией.
Поэтому он не должен напрямую изменять изолированное состояние произвольного актора и не должен содержать долгие или блокирующие действия.
Отмена задач в Swift построена как кооперативный механизм: система устанавливает признак отмены, а сама задача должна реагировать на него. Для интеграции с внешними операциями — сетевыми запросами, таймерами, callback API — нужен отдельный обработчик, который может немедленно запустить их отмену.
Такой обработчик отделён от тела операции, поскольку отменить задачу может другая конкурентная задача. Это позволяет уведомить внешний ресурс даже тогда, когда основная операция в данный момент приостановлена на await.
Нельзя предполагать, что обработчик продолжает изоляцию текущего актора или исполняется последовательно с телом операции. Если он изменяет общие данные без синхронизации, возможна гонка данных; если вызывает акторный метод без корректного перехода, код нарушает правила изоляции.
Есть и логическая гонка: обработчик может выполняться одновременно с завершением операции. Например, операция уже получила результат, пока обработчик отмены пытается закрыть тот же внешний ресурс.
Обработчик отмены — синхронное замыкание. Он запускается, когда задача отменяется во время действия withTaskCancellationHandler; если задача уже была отменена к моменту регистрации обработчика, обработчик также должен быть вызван согласно семантике API.
У обработчика нет гарантии привязки к исполнителю вызывающей задачи. В частности, нахождение вызова внутри метода actor не превращает тело onCancel в actor-изолированный участок. Обработчик должен обращаться только к потокобезопасному состоянию или инициировать безопасную отмену внешней операции.
cancelUnderlyingRequest() должен быть безопасен при конкурентном вызове с завершением request(). Обычно используют потокобезопасный объект отмены, атомарное состояние или API внешней библиотеки, явно допускающий повторный и конкурентный вызов.
Обработчик должен быть коротким: его задача — выставить флаг, вызвать отмену дескриптора или отправить сигнал владельцу ресурса. Долгую работу следует выполнять отдельной задачей или передать изолированному владельцу через безопасный канал.
Сетевой клиент хранит дескриптор запроса внутри actor, а его метод загрузки оборачивает ожидание ответа в withTaskCancellationHandler. Наивный вариант пытается в onCancel напрямую удалить дескриптор из actor-состояния.
Вариант с прямым изменением состояния удобен, но некорректен: обработчик не получает автоматическую actor-изоляцию. Вариант с блокировкой вокруг всего сетевого запроса безопаснее с точки зрения доступа к данным, но может блокировать поток и ухудшить масштабируемость.
Практичное решение — хранить потокобезопасный токен отмены отдельно, зарегистрировать его до начала ожидания и сделать обработчик идемпотентным. При отмене токен закрывает внешний запрос, а actor после завершения операции отдельно очищает своё состояние. Это предотвращает гонки между отменой, ответом сервера и освобождением ресурса.
Вопрос: Гарантирует ли обработчик отмены, что внешняя операция уже завершилась к моменту его возврата?
Ответ: Нет. Обработчик только инициирует отмену. Внешняя операция может завершаться асинхронно, поэтому после возврата из обработчика ресурс ещё может быть занят. Код обязан корректно обрабатывать состояние, в котором отмена и обычное завершение происходят почти одновременно.
Вопрос: Можно ли из обработчика отмены вызвать await, чтобы дождаться освобождения ресурса?
Ответ: Нет, обработчик синхронный и не может приостанавливаться через await. Ожидание нужно выполнять в основной async-операции либо передавать сигнал отдельной задаче. Попытка синхронно ждать завершения может привести к блокировке и взаимной зависимости между отменяющим кодом и отменяемой операцией.
Вопрос: Достаточно ли проверить отмену только внутри обработчика onCancel?
Ответ: Нет. Обработчик вызывается только при отмене в пределах соответствующего участка, а операция может завершиться, приостановиться или получить отмену в другой момент. Основной код должен сам проверять Task.isCancelled или вызывать Task.checkCancellation() в подходящих местах и корректно обрабатывать отменённый результат.