Чтобы возобновить продолжение Swift ровно один раз, заставьте завершение и отмену конкурировать через единый синхронизированный автомат состояний. Сохраняйте отмену, даже если она произошла до установки продолжения, выбирайте один итоговый результат и освобождайте блокировку до возобновления ожидающей задачи.
В этом руководстве создаётся локальный адаптер от обратного вызова к async для программы командной строки на Mac. Синтетическое вычисление удваивает целое число; в примере нет сетевых вызовов, записей или сохранения данных. Основной контракт сформулирован явно: побеждает то терминальное событие, которое первым изменит защищённое состояние.
Какие гарантии должен обеспечивать адаптер?
Проверяемое продолжение связывает синхронные обратные вызовы с асинхронным кодом Swift. Apple требует возобновлять его ровно один раз на каждом пути выполнения. Если этого не сделать, задача останется приостановленной; проверки во время выполнения помогают выявлять неправильное использование, но не реализуют координацию за вас. Проверяемое продолжение
Отмена добавляет ещё одного потенциального владельца этого возобновления. Отмена в Swift кооперативна: отмена задачи автоматически не останавливает произвольную операцию обратного вызова и не заставляет продолжение выбросить ошибку. Адаптер должен явно преобразовать отмену в выбранный им результат. Task.cancel()
Для этого примера контракт результатов таков:
- Входные значения от
-1000до1000дают удвоенное значение. - Для остальных входных значений возвращается
CalculationError.outOfRange. - Если отмена побеждает при переходе состояния, возвращается
CancellationError. - После выбора итогового результата последующие события завершения или отмены не могут его заменить.
- Каждый вызов использует новый шлюз и устанавливает одно продолжение.
Это означает, что завершение может победить, даже если отмена происходит до того, как вызывающая сторона получает значение. Решающий момент — переход защищённого состояния, а не время, когда вызывающая сторона продолжит выполнение. Для этого примера выбрана такая политика приложения; Swift не предоставляет её как универсальную гарантию порядка событий.
Почему отмена может произойти до установки продолжения?
Apple документирует, что обработчик отмены запускается до замыкания операции, если задача уже отменена. При этом сама операция всё равно выполняется. Кроме того, отмена может происходить одновременно с уже выполняющейся операцией. Поэтому обработчик, который просто возобновляет необязательное продолжение, теряет отмену, если это значение ещё не было задано. withTaskCancellationHandler(operation:onCancel:isolation:)
Следовательно, шлюзу нужно знать больше, чем просто наличие продолжения в данный момент. Отсутствие продолжения может означать, что установка ещё не произошла, или что итоговый результат уже выбран. В этих случаях поведение должно различаться.
| Защищённое состояние | Установить продолжение | Получить завершение или отмену |
|---|---|---|
| Пустое | Сохранить продолжение | Сохранить итоговый результат |
| Ожидание | Отклонить повторную установку | Извлечь продолжение и сохранить итоговый результат |
| Завершено | Возобновить вновь установленное продолжение сохранённым результатом | Игнорировать последующее терминальное событие |
Таблица также помогает диагностировать ошибки. Если реализация не может представить итоговый результат до установки продолжения, проверьте путь уже отменённой задачи. Если оба терминальных события могут извлечь продолжение, проверьте, выполняются ли проверка и извлечение в одной критической секции.
Создайте локальный пример на Mac
Используйте инструментарий Swift 6 с SDK для macOS. Проверьте выбранный инструментарий в Terminal командой swift --version. Программа ниже использует только Foundation и Dispatch; зависимости пакетов не нужны. В руководстве Swift для командной строки описано, как проверить установку и использовать асинхронную точку входа @main. Создание инструмента командной строки
Сохраните следующие три блока Swift по порядку в один файл с именем ContinuationRace.swift. Вместе они образуют полную программу. Первый блок реализует шлюз, второй содержит вычисление и адаптер, а третий — исполняемые проверки. В исследовательской среде Swift недоступен, поэтому программа была просмотрена, но не скомпилирована и не запускалась.
Реализуйте синхронизированный шлюз
Apple описывает NSLock как средство защиты критической секции. Блокировка и разблокировка должны выполняться в одном потоке, а сама блокировка не является рекурсивной. Здесь каждая критическая секция находится внутри синхронного метода, не содержит точки приостановки и завершается до возобновления продолжения. NSLock
import Foundation
import Dispatch
final class IntContinuationGate: @unchecked Sendable {
private enum State {
case empty
case waiting(CheckedContinuation<Int, any Error>)
case finished(Result<Int, any Error>)
}
private let lock = NSLock()
private var state: State = .empty
private var didInstall = false
// Returns false if a terminal event already won.
func install(
_ continuation: CheckedContinuation<Int, any Error>
) -> Bool {
lock.lock()
precondition(!didInstall, "Install exactly once per gate")
didInstall = true
switch state {
case .empty:
state = .waiting(continuation)
lock.unlock()
return true
case .waiting:
preconditionFailure("Unexpected second installation")
case .finished(let result):
lock.unlock()
continuation.resume(with: result)
return false
}
}
func finish(_ result: Result<Int, any Error>) {
let continuation: CheckedContinuation<Int, any Error>?
lock.lock()
switch state {
case .empty:
continuation = nil
state = .finished(result)
case .waiting(let waiting):
continuation = waiting
state = .finished(result)
case .finished:
lock.unlock()
return
}
lock.unlock()
continuation?.resume(with: result)
}
func cancel() {
finish(.failure(CancellationError()))
}
}
Шлюз использует @unchecked Sendable, поскольку его изменяемое хранилище защищено вручную. Эта аннотация возлагает ответственность на реализацию, но не добавляет синхронизацию. Оба изменяемых свойства остаются закрытыми, и каждый доступ к ним выполняется под одной и той же блокировкой. Apple прямо указывает, что защищённое блокировкой состояние позволяет выполнить семантические требования unchecked-передаваемости. Sendable
Обратите внимание: finish изменяет состояние до снятия блокировки. После этого конкурирующее терминальное событие может увидеть только finished, даже если выигравший поток ещё не вызвал resume. Победитель сохраняет извлечённое продолжение локально; ни один конкурирующий вызов не сможет извлечь его повторно.
Свяжите завершение и отмену с одним шлюзом
В вычислении обратного вызова используется глобальная очередь отправки. Apple указывает, что асинхронно отправленная работа выполняется отдельно от кода, который её отправил, а для фоновой работы Dispatch идентификатор потока не гарантируется. Поэтому здесь подходит координация, не зависящая от потока. DispatchQueue
enum CalculationError: Error {
case outOfRange
}
func startDoubling(
_ value: Int,
completion: @escaping @Sendable (Result<Int, any Error>) -> Void
) {
DispatchQueue.global().async {
guard (-1000...1000).contains(value) else {
completion(.failure(CalculationError.outOfRange))
return
}
completion(.success(value * 2))
}
}
func doubled(_ value: Int) async throws -> Int {
let gate = IntContinuationGate()
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Int, any Error>) in
guard gate.install(continuation) else { return }
startDoubling(value) { result in
gate.finish(result)
}
}
} onCancel: {
gate.cancel()
}
}
Создайте шлюз до входа в область обработчика отмены. Тогда уже отменённая задача сможет немедленно записать отмену. Когда продолжение будет установлено позже, установка возобновит его с сохранённой ошибкой и вернёт false, поэтому адаптер не запустит вычисление.
Отмена всё ещё может произойти после того, как установка вернёт true, и до вызова startDoubling. При таком порядке вычисление запускается и завершается, но его результат игнорируется, поскольку отмена уже победила. Обёртка отменяет ожидание, но не обещает остановить это небольшое ограниченное по объёму вычисление.
Поддерживаемый мост для продолжений — withCheckedThrowingContinuation; его замыкание получает продолжение, используемое для возврата значения или выбрасывания ошибки. В примере явно указаны целочисленный тип результата продолжения и тип ошибки, чтобы избежать неоднозначного вывода типов. withCheckedThrowingContinuation(isolation:function:_:)
Проверьте порядок событий без задержек
Задержка по времени не определяет, какое событие выиграло гонку. Вместо этого следующие проверки напрямую проводят через шлюз все шесть вариантов порядка установки, завершения и отмены. Для каждой последовательности также подаются дополнительные терминальные события, чтобы проверить, что выбранный результат остаётся неизменным.
enum GateEvent {
case install
case complete
case cancel
}
func checkOrder(
_ events: [GateEvent],
expected: Int?
) async throws {
let gate = IntContinuationGate()
do {
let value: Int = try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Int, any Error>) in
for event in events {
switch event {
case .install:
_ = gate.install(continuation)
case .complete:
gate.finish(.success(42))
case .cancel:
gate.cancel()
}
}
// Neither of these may replace the selected result.
gate.finish(.success(99))
gate.cancel()
}
guard let expected else {
preconditionFailure("Expected cancellation")
}
precondition(value == expected, "Unexpected value")
} catch is CancellationError {
precondition(expected == nil, "Unexpected cancellation")
}
}
@main
struct ContinuationRaceDemo {
static func main() async throws {
try await checkOrder(
[.install, .complete, .cancel], expected: 42
)
try await checkOrder(
[.install, .cancel, .complete], expected: nil
)
try await checkOrder(
[.complete, .install, .cancel], expected: 42
)
try await checkOrder(
[.complete, .cancel, .install], expected: 42
)
try await checkOrder(
[.cancel, .install, .complete], expected: nil
)
try await checkOrder(
[.cancel, .complete, .install], expected: nil
)
let value = try await doubled(21)
precondition(value == 42)
do {
_ = try await doubled(1001)
preconditionFailure("Expected outOfRange")
} catch CalculationError.outOfRange {
// Expected input-domain error.
}
// Integration smoke check: either terminal outcome is valid.
for _ in 0..<100 {
let task = Task { try await doubled(21) }
task.cancel()
do {
let result = try await task.value
precondition(result == 42)
} catch is CancellationError {
// Cancellation won.
}
}
print("Event-order checks and integration checks passed.")
}
}
Скомпилируйте и запустите программу из каталога с файлом:
swiftc -swift-version 6 -parse-as-library ContinuationRace.swift -o ContinuationRace
./ContinuationRace
Ожидаемый успешный вывод: Event-order checks and integration checks passed. Явные проверки порядка подтверждают переходы состояний. Цикл интеграционных проверок использует настоящий адаптер, но не гарантирует наступления конкретного порядка выполнения и не доказывает, что проверено каждое конкурентное выполнение.
Почему реализация возобновляет продолжение ровно один раз?
Чтобы рассмотреть гарантию «не более одного раза», возьмём первый успешный вызов finish. Он атомарно заменяет текущее состояние на finished. Если продолжение ожидает, только этот вызов извлечёт его. Каждый последующий вызов finish возвращается, ничего не возобновляя. Если продолжения ещё нет, результат ожидает единственной установки.
Гарантия eventual resumption обеспечивается двумя путями. Сначала происходит установка, и первое терминальное событие возобновляет ожидающее продолжение; либо сначала происходит терминальное событие, и установка возобновляет продолжение сохранённым результатом. Это рассуждение предполагает, что установка происходит один раз и что возникает хотя бы одно терминальное событие. Если общий источник обратных вызовов никогда не завершается, а задачу никогда не отменяют, вызывающая сторона так и будет ждать.
В примере обратный вызов выполняет ограниченное вычисление и вызывает завершение как при успехе, так и при ошибке входных данных. Повторные терминальные обратные вызовы намеренно игнорируются после выбора победителя. Это подходит для заданного контракта с одним результатом; для потока значимых значений потребуется другой интерфейс.
Какие упрощённые решения нарушают контракт?
Проверить необязательное продолжение, а затем присвоить ему nil недостаточно, если эти действия не синхронизированы. Два конкурирующих вызова могут прочитать одно и то же продолжение, отличное от nil, прежде чем хотя бы один из них его извлечёт. Проверка, извлечение и изменение терминального состояния должны быть одной защищённой операцией.
Проверка отмены до создания продолжения также оставляет временной промежуток: отмена может произойти сразу после проверки. Обработчик и сохранение терминального состояния устраняют этот пробел. В руководстве по языку Swift отмена описывается как кооперативная, а обработчики отмены указываются как механизм немедленного уведомления. Конкурентность — язык программирования Swift
Возобновление под общей блокировкой создаёт ещё одну проблему. Apple специально предупреждает, что обработчики отмены могут запускаться, когда удерживаются внутренние блокировки задачи, а возобновление продолжения может захватывать эти блокировки. Поэтому в примере перед каждым вызовом resume блокировка снимается. Обработчики отмены и блокировки
Наконец, добавление Task.checkCancellation() после возврата продолжения изменило бы внешний контракт этого примера: отмена могла бы заменить значение, даже если шлюз уже выбрал завершение. Выбирайте такую политику осознанно. Для реализованного здесь контракта установка, завершение и отмена используют один автомат состояний, а результат вызывающей стороны определяется первым терминальным переходом.
Источники
- Проверяемое продолжение — Apple.
- withTaskCancellationHandler(operation:onCancel:isolation:) — Apple.
- cancel() — Apple.
- NSLock — Apple.
- Sendable — Apple.
- DispatchQueue — Apple.
- withCheckedThrowingContinuation(isolation:function:_:) — Apple.
- Создание инструмента командной строки — Swift.org.
- Конкурентность — язык программирования Swift — Проект Swift.