Om een Swift-continuation precies één keer te hervatten, laat je voltooiing en annulering met elkaar concurreren via één gesynchroniseerde toestandsmachine. Sla annulering ook op vóór installatie, kies één eindresultaat en laat de lock los voordat je de wachtende taak hervat.
Deze tutorial bouwt een lokale adapter van callback naar async voor een opdrachtregelprogramma voor de Mac. De synthetische berekening verdubbelt een geheel getal; het voorbeeld gebruikt geen netwerkoproepen, records of persistentie. Het centrale contract is expliciet: de eerste terminale gebeurtenis die de beschermde toestand wijzigt, wint.
Welke garanties moet de adapter bieden?
Een gecontroleerde continuation verbindt synchrone callbacks met asynchrone Swift-code. Apple vereist dat elk uitvoeringspad deze precies één keer hervat. Als hervatting uitblijft, blijft de taak opgeschort; runtimecontroles helpen misbruik op te sporen, maar regelen de coördinatie niet voor je. CheckedContinuation
Annulering introduceert een mogelijke tweede eigenaar van die hervatting. Annulering in Swift is coöperatief: het annuleren van een taak stopt niet automatisch een willekeurige callbackbewerking en zorgt er ook niet voor dat een continuation een fout genereert. De adapter moet annulering expliciet omzetten in het gekozen resultaat. Task.cancel()
Voor dit voorbeeld is het contract voor waarden als volgt:
- Invoer van
-1000tot en met1000levert het dubbele van de waarde op. - Andere invoer levert
CalculationError.outOfRangeop. - Annulering levert
CancellationErrorop als die de statusovergang wint. - Zodra een eindresultaat is gekozen, kunnen latere voltooiings- of annuleringsgebeurtenissen dit niet vervangen.
- Elke aanroep heeft een nieuwe gate en installeert één continuation.
Dit betekent dat voltooiing kan winnen, zelfs wanneer annulering plaatsvindt voordat de aanroeper de waarde ontvangt. Het beslissende moment is de statusovergang binnen de beveiligde sectie, niet het moment waarop de aanroeper daarna weer wordt uitgevoerd. Dit is een toepassingsbeleid dat voor het voorbeeld is gekozen; Swift biedt geen universele garantie voor de volgorde.
Waarom kan annulering plaatsvinden vóór de installatie?
Apple documenteert dat de annuleringshandler vóór de bewerkingsclosure wordt uitgevoerd als de taak al is geannuleerd. De bewerking wordt nog steeds uitgevoerd. Annulering kan ook gelijktijdig plaatsvinden met een bewerking die al bezig is. Daardoor gaat annulering verloren als een handler alleen een optionele continuation hervat terwijl die optionele nog niet is ingevuld. withTaskCancellationHandler(operation:onCancel:isolation:)
De gate heeft daarom meer informatie nodig dan alleen of er op dat moment een continuation bestaat. Als er geen continuation is, kan dat betekenen dat de installatie nog niet heeft plaatsgevonden, of dat er al een eindresultaat is gekozen. Die gevallen vereisen verschillend gedrag.
| Beschermde toestand | De continuation installeren | Voltooiing of annulering ontvangen |
|---|---|---|
| Leeg | De continuation opslaan | Het eindresultaat opslaan |
| Wachtend | Een tweede installatie afwijzen | De continuation verwijderen en het eindresultaat opslaan |
| Voltooid | De nieuw geïnstalleerde continuation hervatten met het opgeslagen resultaat | De latere terminale gebeurtenis negeren |
De tabel is ook een hulpmiddel bij het opsporen van fouten. Als een implementatie geen eindresultaat kan vastleggen vóór de installatie, controleer dan het pad voor taken die al zijn geannuleerd. Als twee terminale gebeurtenissen allebei de continuation kunnen ophalen, controleer dan of de controle en verwijdering binnen één kritieke sectie plaatsvinden.
Bouw een lokaal voorbeeld op je Mac
Gebruik een Swift 6-toolchain met de macOS SDK. Controleer de geselecteerde toolchain in Terminal met swift --version. Het onderstaande programma gebruikt alleen Foundation en Dispatch; er zijn geen pakketafhankelijkheden nodig. De opdrachtregelhandleiding van Swift beschrijft hoe je de installatie controleert en een asynchroon @main-invoerpunt gebruikt. Build a Command-line Tool
Sla de volgende drie Swift-blokken in deze volgorde op in één bestand met de naam ContinuationRace.swift. Samen vormen ze één compleet programma. Het eerste blok implementeert de gate, het tweede bevat de berekening en adapter, en het derde bevat uitvoerbare controles. Swift is niet beschikbaar in de onderzoeksomgeving, dus dit programma is hier nagekeken, maar niet gecompileerd of uitgevoerd.
Implementeer de gesynchroniseerde gate
Apple documenteert NSLock als een manier om een kritieke sectie te beveiligen. Vergrendelen en ontgrendelen moeten op dezelfde thread gebeuren, en de lock is niet recursief. Hier blijft elke kritieke sectie binnen een synchrone methode, bevat die geen opschortingspunt en eindigt die voordat de continuation wordt hervat. 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()))
}
}
De gate gebruikt @unchecked Sendable omdat de veranderlijke opslag handmatig wordt beveiligd. Met die annotatie neemt de implementatie de verantwoordelijkheid op zich; de annotatie voegt geen synchronisatie toe. Beide veranderlijke eigenschappen blijven privé en elke toegang vindt plaats onder dezelfde lock. Apple beschrijft expliciet dat met een lock beveiligde toestand kan voldoen aan de semantische vereisten voor unchecked sendability. Sendable
Merk op dat finish de toestand wijzigt voordat de lock wordt ontgrendeld. Een concurrerende terminale gebeurtenis kan dan alleen finished zien, zelfs als de winnende thread nog niet resume heeft aangeroepen. De winnaar houdt de opgehaalde continuation lokaal vast; geen enkele concurrerende aanroep kan die opnieuw ophalen.
Verbind voltooiing en annulering met dezelfde gate
De callbackberekening gebruikt een globale dispatchwachtrij. Apple documenteert dat asynchroon ingediend werk afzonderlijk van de code die het indient wordt uitgevoerd en dat er geen vaste threadidentiteit is voor achtergrondwerk via dispatch. Daarom is coördinatie die onafhankelijk is van de thread hier geschikt. 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()
}
}
Maak de gate aan voordat je de scope van de annuleringshandler binnengaat. Zo kan een al geannuleerde taak annulering direct vastleggen. Wanneer de continuation daarna wordt geïnstalleerd, hervat de installatie deze met die opgeslagen fout en geeft false terug, zodat de adapter de berekening niet start.
Annulering kan nog steeds plaatsvinden nadat de installatie true heeft teruggegeven, maar voordat startDoubling wordt aangeroepen. In die volgorde start en eindigt de berekening, maar wordt het resultaat genegeerd omdat annulering al heeft gewonnen. De wrapper annuleert het wachten; die belooft niet deze kleine, begrensde berekening te stoppen.
De ondersteunde continuationbrug is withCheckedThrowingContinuation; de closure ervan ontvangt de continuation die wordt gebruikt om een waarde terug te geven of een fout te genereren. Het voorbeeld vermeldt expliciet de typen van de integerresultaten en fouten van de continuation, zodat type-inferentie eenduidig is. withCheckedThrowingContinuation(isolation:function:_:)
Controleer de gebeurtenisvolgorde zonder op slaapvertragingen te vertrouwen
Een tijdsvertraging bepaalt niet welke gebeurtenis een race wint. De volgende controles doorlopen in plaats daarvan rechtstreeks via de gate alle zes volgordes van installatie, voltooiing en annulering. Elke reeks krijgt ook extra terminale gebeurtenissen om te controleren of de gekozen uitkomst ongewijzigd blijft.
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.")
}
}
Compileer en voer het programma uit vanuit de map met het bestand:
swiftc -swift-version 6 -parse-as-library ContinuationRace.swift -o ContinuationRace
./ContinuationRace
De verwachte succesvolle uitvoer is Event-order checks and integration checks passed. De expliciete volgordecontroles valideren de statusovergangen. De integratielus oefent de echte adapter, maar garandeert niet dat een bepaalde planningsinterleaving plaatsvindt en bewijst niet dat elke gelijktijdige uitvoering is getest.
Waarom hervat de implementatie precies één keer?
Kijk voor hervatting hooguit één keer naar de eerste geslaagde aanroep van finish. Deze vervangt de huidige toestand atomair door finished. Als er een continuation wacht, haalt alleen die aanroep haar op. Elke latere aanroep van finish keert terug zonder iets te hervatten. Als er nog geen continuation bestaat, wacht het resultaat op de enige installatie.
Voor uiteindelijke hervatting zijn er twee paden. Eerst vindt de installatie plaats, waarna de eerste terminale gebeurtenis de wachtende continuation hervat; of eerst vindt een terminale gebeurtenis plaats, waarna de installatie hervat met het opgeslagen resultaat. Deze redenering gaat ervan uit dat de installatie één keer plaatsvindt en dat er minstens één terminale gebeurtenis optreedt. Een algemene callbackbron die nooit wordt voltooid, in combinatie met een taak die nooit wordt geannuleerd, laat de aanroeper nog steeds wachten.
De callback in het voorbeeld voert een begrensde berekening uit en roept de voltooiingsfunctie aan bij zowel succes als een invoerfout. Dubbele terminale callbacks worden na de winnaar bewust genegeerd. Dit past bij het opgegeven contract met één resultaat; voor een stroom met betekenisvolle waarden is een andere interface nodig.
Welke shortcuts schenden het contract?
Een optionele continuation controleren en die daarna instellen op nil is onvoldoende als die acties niet zijn gesynchroniseerd. Twee gelijktijdige aanroepers kunnen allebei dezelfde niet-nil continuation lezen voordat een van beiden die verwijdert. De controle, het ophalen en de wijziging van de terminale toestand moeten één beveiligde bewerking vormen.
Een controle op annulering vóór het aanmaken van de continuation laat ook een opening: annulering kan direct na die controle plaatsvinden. De handler en de onthouden terminale toestand dichten die opening. De taalgids van Swift beschrijft annulering als coöperatief en noemt annuleringshandlers als mechanisme voor onmiddellijke melding. Concurrency — The Swift Programming Language
Hervatten terwijl de gedeelde lock is vergrendeld, veroorzaakt een ander probleem. Apple waarschuwt specifiek dat annuleringshandlers kunnen worden uitgevoerd terwijl interne taaklocks vergrendeld zijn, en dat het hervatten van een continuation die locks kan verkrijgen. Daarom ontgrendelt het voorbeeld de lock vóór elke aanroep van resume. Cancellation handlers and locks
Tot slot zou het toevoegen van Task.checkCancellation() nadat de continuation is teruggekeerd het externe contract van dit voorbeeld wijzigen: annulering zou een waarde kunnen vervangen, zelfs nadat voltooiing de gate heeft gewonnen. Kies dat beleid bewust. Voor het hier geïmplementeerde contract gebruiken installatie, voltooiing en annulering dezelfde toestandsmachine, en bepaalt de eerste terminale overgang het resultaat voor de aanroeper.
Bronnen
- 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.