Как async-обёртка над callback API должна передать отмену родительской задачи в незавершённую операцию?
Обёртка должна явно связать отмену Task с отменой underlying-операции через withTaskCancellationHandler или эквивалентный механизм. Сама отмена задачи не останавливает callback API автоматически: она лишь устанавливает флаг отмены, который должны проверить задача и подключённая операция.
При завершении операции continuation нужно возобновить ровно один раз — результатом, ошибкой или ошибкой отмены. Обработчик отмены должен быть безопасен при гонке с обычным завершением запроса.
Многие системные и сторонние API появились до async/await и сообщают о результате через callback. У таких API обычно есть собственный объект операции и отдельный метод отмены, тогда как конкурентность Swift управляет только жизненным циклом Task.
Structured concurrency добавляет отмену как кооперативный механизм, но не может автоматически догадаться, как остановить произвольную legacy-операцию. Поэтому адаптер обязан явно связать два жизненных цикла.
Если async-функция только ожидает callback, а вызывающая задача уже отменена, сетевой запрос, чтение файла или вычисление могут продолжаться. Это расходует ресурсы, может выполнять ненужные побочные эффекты и удерживать объекты дольше ожидаемого.
Обратная ошибка тоже опасна: callback может прийти одновременно с отменой. Тогда один путь попытается вернуть успешный результат, а другой — ошибку отмены. Двойное возобновление continuation нарушает её контракт и в проверяемой continuation приводит к аварийному завершению.
Для адаптации используются три элемента:
Важно, что обработчик отмены может выполниться параллельно с операцией и не обязан ждать её завершения. Поэтому метод cancel должен быть безопасным при повторном вызове, а состояние адаптера — синхронизированным. Если API не допускает безопасную отмену после завершения, адаптеру нужен отдельный потокобезопасный объект состояния.
Минимальная форма адаптера выглядит так:
Здесь LegacyRequest — API, у которого start асинхронно вызывает completion, а cancel останавливает операцию. Реальный адаптер должен дополнительно гарантировать, что cancel и completion не вызывают resume дважды; сам пример показывает только направление передачи отмены.
Отмена не означает, что функция немедленно прекратит выполнение. Если операция уже завершилась или callback API игнорирует отмену, результат может быть доставлен позже. Адаптер должен решить, какой результат считать главным при гонке: обычно используется первое зафиксированное завершение, а последующие callback игнорируются.
Если underlying API не умеет отменяться, обработчик всё равно может позволить async-функции завершиться ошибкой отмены, но фоновая работа продолжится. Это снижает отзывчивость вызывающего кода, однако не освобождает от необходимости защищать ресурсы и игнорировать запоздалый callback.
Экран поиска запускает async-функцию для каждого введённого запроса. Пользователь быстро вводит новый текст, поэтому предыдущая задача отменяется. Legacy-клиент продолжает сетевой запрос и позже вызывает callback.
Вариант без обработчика отмены прост, но старые запросы расходуют соединения и могут продолжать побочные действия. Вариант с немедленным вызовом continuation из обработчика отмены быстрее освобождает ожидающую задачу, но требует сложной синхронизации: callback может прийти одновременно, а continuation разрешено возобновить только один раз.
Практическое решение — передать отмену в объект запроса, сделать его cancel идемпотентным и централизовать выбор первого завершения в потокобезопасном состоянии. При отмене старый запрос прекращается настолько быстро, насколько позволяет API, его запоздалый callback игнорируется, а новый запрос получает актуальный результат.
1. Достаточно ли проверить Task.isCancelled после ожидания callback?
Нет. Такая проверка позволяет не использовать устаревший результат, но не останавливает саму legacy-операцию. Запрос или вычисление продолжит занимать ресурсы до естественного завершения, если адаптер не вызовет его API отмены.
Проверка полезна как дополнительная защита перед применением результата. Она не заменяет withTaskCancellationHandler, потому что не передаёт сигнал отмены наружу.
2. Можно ли безусловно возобновить continuation ошибкой отмены внутри обработчика отмены?
Только если адаптер надёжно исключает одновременный callback и повторное завершение. В общем случае это небезопасно: обработчик отмены и completion могут выполняться на разных потоках.
Нужен единый потокобезопасный механизм «завершено или нет». Первый победивший путь возобновляет continuation, остальные только освобождают свои ресурсы или игнорируются.
3. Что делать, если отмена произошла до запуска legacy-операции?
Обработчик должен учитывать эту гонку. Задача могла быть отменена до входа в тело операции, поэтому запрос может оказаться запущенным уже после установки флага отмены.
Надёжный адаптер проверяет состояние отмены при создании запроса и перед стартом, а также вызывает cancel, если отмена была зафиксирована раньше. Простого вызова cancel только в будущем обработчике недостаточно, если операция ещё не существует или стартует после этого момента.