Para retomar uma continuação do Swift exatamente uma vez, faça com que a conclusão e o cancelamento disputem por meio de uma única máquina de estados sincronizada. Registre o cancelamento mesmo antes da instalação, selecione um único resultado terminal e libere o bloqueio antes de retomar a tarefa em espera.

Este tutorial cria um adaptador local de callback para async para um programa de linha de comando do Mac. Seu cálculo sintético dobra um número inteiro; o exemplo não usa chamadas de rede, registros nem persistência. O contrato central é explícito: vence o primeiro evento terminal que altera o estado protegido.

Que garantias o adaptador precisa oferecer?

Uma continuação verificada conecta callbacks síncronos ao código assíncrono do Swift. A Apple exige que cada caminho de execução a retome exatamente uma vez. Se ela não for retomada, a tarefa fica suspensa; a verificação em tempo de execução ajuda a identificar o uso indevido, mas não implementa a coordenação por você. CheckedContinuation

O cancelamento introduz outro possível responsável por essa retomada. O cancelamento no Swift é cooperativo: cancelar uma tarefa não interrompe automaticamente uma operação arbitrária de callback nem faz uma continuação lançar um erro. O adaptador precisa transformar explicitamente o cancelamento no resultado escolhido. Task.cancel()

Neste exemplo, o contrato dos valores é:

  • As entradas de -1000 a 1000 produzem o dobro do seu valor.
  • Outras entradas produzem CalculationError.outOfRange.
  • O cancelamento produz CancellationError se vencer a transição de estado.
  • Depois que um resultado terminal é selecionado, eventos posteriores de conclusão ou cancelamento não podem substituí-lo.
  • Cada invocação tem seu próprio gate e instala uma única continuação.

Isso significa que a conclusão pode vencer mesmo quando o cancelamento ocorre antes de quem chamou receber o valor. O momento decisivo é a transição de estado protegida, e não o momento em que quem chamou volta a executar. Essa é uma política específica do exemplo, não uma garantia universal de ordenação fornecida pelo Swift.

Por que o cancelamento pode chegar antes da instalação?

A Apple documenta que o manipulador de cancelamento é executado antes do closure de operação quando a tarefa já está cancelada. Ainda assim, a operação é executada. O cancelamento também pode ocorrer em paralelo a uma operação já em andamento. Por isso, um manipulador que simplesmente retoma uma continuação opcional perde o cancelamento quando essa opção ainda não foi preenchida. withTaskCancellationHandler(operation:onCancel:isolation:)

Portanto, o gate precisa de mais informações do que saber se há uma continuação no momento. A ausência de uma continuação pode significar que a instalação ainda não ocorreu ou que um resultado terminal já foi selecionado. Esses casos exigem comportamentos diferentes.

Estado protegido Instalar a continuação Receber conclusão ou cancelamento
Vazio Armazenar a continuação Armazenar o resultado terminal
Aguardando Rejeitar uma segunda instalação Remover a continuação e armazenar o resultado terminal
Concluído Retomar a continuação recém-instalada com o resultado armazenado Ignorar o evento terminal posterior

A tabela também ajuda no diagnóstico. Se uma implementação não consegue representar um resultado terminal antes da instalação, examine o caminho em que a tarefa já está cancelada. Se dois eventos terminais podem extrair a continuação, verifique se a checagem e a remoção ocorrem na mesma seção crítica.

Crie um exemplo local no Mac

Use uma toolchain do Swift 6 com o SDK do macOS. Confira a toolchain selecionada no Terminal com swift --version. O programa abaixo usa apenas Foundation e Dispatch; não precisa de dependências de pacotes. O guia de linha de comando do Swift documenta como verificar a instalação e usar um ponto de entrada assíncrono @main. Build a Command-line Tool

Salve os três blocos Swift a seguir, na ordem, em um único arquivo chamado ContinuationRace.swift. Juntos, eles formam um programa completo. O Swift não está disponível no ambiente de pesquisa, então o programa foi inspecionado, mas não foi compilado nem executado aqui.

Implemente o gate sincronizado

A Apple documenta NSLock como uma forma de proteger uma seção crítica. O bloqueio e o desbloqueio devem ocorrer na mesma thread, e o bloqueio não é recursivo. Aqui, cada seção crítica permanece dentro de um método síncrono, não contém pontos de suspensão e termina antes da retomada da continuação. 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()))
    }
}

O gate usa @unchecked Sendable porque seu armazenamento mutável é protegido manualmente. Essa anotação transfere a responsabilidade à implementação; ela não adiciona sincronização. Ambas as propriedades mutáveis permanecem privadas, e todo acesso a elas ocorre sob o mesmo bloqueio. A Apple descreve explicitamente o estado protegido por bloqueio como uma forma de atender aos requisitos semânticos da conformidade não verificada com Sendable. Sendable

Observe que finish altera o estado antes de desbloquear. Assim, um evento terminal concorrente só pode observar finished, mesmo que a thread vencedora ainda não tenha chamado resume. A thread vencedora mantém localmente a continuação extraída; nenhuma chamada concorrente pode extraí-la novamente.

Conecte a conclusão e o cancelamento ao mesmo gate

O cálculo via callback usa uma fila global de dispatch. A Apple documenta que o trabalho enviado de forma assíncrona prossegue separadamente do código que o enviou e que o trabalho de dispatch em segundo plano não tem identidade de thread garantida. Por isso, a coordenação independente de thread é apropriada aqui. 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()
    }
}

Crie o gate antes de entrar no escopo do manipulador de cancelamento. Assim, uma tarefa já cancelada pode registrar o cancelamento imediatamente. Quando a continuação é instalada depois, o gate a retoma com a falha armazenada e retorna false, então o adaptador não inicia o cálculo.

O cancelamento ainda pode ocorrer depois que a instalação retorna true e antes da chamada a startDoubling. Nessa ordem, o cálculo começa e termina, mas seu resultado é ignorado porque o cancelamento já venceu. O wrapper cancela a espera; ele não promete interromper esse pequeno cálculo de duração limitada.

A ponte compatível para continuações é withCheckedThrowingContinuation; seu closure recebe a continuação usada para retornar um valor ou lançar um erro. O exemplo especifica os tipos de resultado inteiro e de erro da continuação para evitar ambiguidades na inferência. withCheckedThrowingContinuation(isolation:function:_:)

Verifique a ordem dos eventos sem depender de pausas

Uma pausa não determina qual evento venceu a corrida. Em vez disso, as verificações a seguir percorrem diretamente pelo gate as seis ordens possíveis de instalação, conclusão e cancelamento. Cada sequência também recebe eventos terminais extras para verificar se o resultado selecionado permanece inalterado.

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.")
    }
}

Compile e execute no diretório que contém o arquivo:

swiftc -swift-version 6 -parse-as-library ContinuationRace.swift -o ContinuationRace
./ContinuationRace

A saída esperada em caso de sucesso é Event-order checks and integration checks passed. As verificações explícitas da ordem validam as transições de estado. O loop de integração exercita o adaptador real, mas não garante que ocorra uma intercalação específica do agendamento nem prova que todas as execuções concorrentes tenham sido testadas.

Por que a implementação retoma exatamente uma vez?

Para garantir no máximo uma retomada, considere a primeira chamada bem-sucedida a finish. Ela substitui atomicamente o estado atual por finished. Se houver uma continuação em espera, somente essa chamada a extrai. Cada chamada posterior a finish retorna sem retomar nada. Se ainda não houver uma continuação, o resultado fica aguardando a única instalação.

Para garantir a retomada eventual, há dois caminhos. A instalação ocorre primeiro, e o primeiro evento terminal retoma a continuação em espera; ou um evento terminal ocorre primeiro, e a instalação retoma a continuação com o resultado armazenado. Esse raciocínio pressupõe que a instalação ocorre uma vez e que pelo menos um evento terminal acontece. Uma fonte genérica de callbacks que nunca conclui, combinada a uma tarefa que nunca é cancelada, ainda deixaria quem chamou esperando.

O callback do exemplo executa um cálculo de duração limitada e chama a conclusão tanto no caminho de sucesso quanto no de erro de entrada. Callbacks terminais duplicados são intencionalmente ignorados depois que há um vencedor. Isso é adequado para o contrato especificado de resultado único; um fluxo de valores significativos exigiria outra interface.

Quais atalhos quebram o contrato?

Verificar uma continuação opcional e depois defini-la como nil não é suficiente quando essas ações não estão sincronizadas. Duas chamadas concorrentes podem ler a mesma continuação diferente de nil antes que qualquer uma a remova. A verificação, a extração e a alteração do estado terminal precisam constituir uma única operação protegida.

Verificar o cancelamento antes de criar a continuação também deixa uma brecha: o cancelamento pode chegar imediatamente após essa verificação. O manipulador e o estado terminal armazenado fecham essa brecha. O guia da linguagem Swift descreve o cancelamento como cooperativo e identifica os manipuladores de cancelamento como o mecanismo de notificação imediata. Concurrency — The Swift Programming Language

Retomar enquanto mantém o bloqueio compartilhado introduz outro problema. A Apple alerta especificamente que os manipuladores de cancelamento podem ser executados enquanto bloqueios internos da tarefa estão mantidos e que a retomada da continuação pode adquirir esses bloqueios. Por isso, o exemplo desbloqueia antes de cada chamada a resume. Cancellation handlers and locks

Por fim, adicionar Task.checkCancellation() depois que a continuação retorna alteraria o contrato externo deste exemplo: o cancelamento poderia substituir um valor mesmo depois que a conclusão venceu no gate. Defina essa política deliberadamente. No contrato implementado aqui, instalação, conclusão e cancelamento usam uma única máquina de estados, e sua primeira transição terminal determina o resultado de quem chamou.

Fontes