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
-1000a1000produzem o dobro do seu valor. - Outras entradas produzem
CalculationError.outOfRange. - O cancelamento produz
CancellationErrorse 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
- CheckedContinuation — Apple.
- withTaskCancellationHandler(operation:onCancel:isolation:) — Apple.
- cancel() — Apple.
- NSLock — Apple.
- Sendable — Apple.
- DispatchQueue — Apple.
- withCheckedThrowingContinuation(isolation:function:_:) — Apple.
- Build a Command-line Tool — Swift.org.
- Concurrency — The Swift Programming Language — The Swift Project.