För att återuppta en Swift-continuation exakt en gång låter du slutförande och avbrytning konkurrera genom en synkroniserad tillståndsmaskin. Spara avbrytningen även innan continuationen installeras, välj ett enda sluttillstånd och släpp låset innan den väntande uppgiften återupptas.
Den här handledningen bygger en lokal adapter från callback till async för ett kommandoradsprogram på Mac. Den syntetiska beräkningen dubblar ett heltal; exemplet använder inga nätverksanrop, poster eller beständig lagring. Grundregeln är tydlig: den terminala händelse som först ändrar det skyddade tillståndet vinner.
Vilka garantier måste adaptern ge?
En kontrollerad continuation kopplar synkrona callbacks till asynkron Swift-kod. Apple kräver att den återupptas exakt en gång på varje exekveringsväg. Om den inte återupptas förblir uppgiften pausad; körningstidskontroller hjälper till att upptäcka felaktig användning men samordnar inte händelserna åt dig. Kontrollerad continuation
Avbrytning innebär att ytterligare en aktör kan försöka återuppta den. Avbrytning i Swift är kooperativ: att avbryta en uppgift stoppar inte automatiskt en godtycklig callback-operation och gör inte att en continuation kastar ett fel. Adaptern måste uttryckligen omvandla avbrytningen till det resultat den har valt. Task.cancel()
Värdekontraktet för det här exemplet är följande:
- Indata från
-1000till1000ger sitt dubbla värde. - Annan indata ger
CalculationError.outOfRange. - Avbrytning ger
CancellationErrorom den vinner tillståndsövergången. - När ett sluttillstånd har valts kan senare händelser för slutförande eller avbrytning inte ersätta det.
- Varje anrop äger en ny spärr och installerar en continuation.
Det innebär att slutförandet kan vinna även om avbrytningen sker innan anroparen får värdet. Det avgörande ögonblicket är den skyddade tillståndsövergången, inte när anroparen körs igen. Detta är en tillämpningspolicy som valts för exemplet, inte en universell ordningsgaranti från Swift.
Varför kan avbrytning ske innan installationen?
Apple dokumenterar att avbrytningshanteraren körs före operationsblocket när uppgiften redan har avbrutits. Operationen körs ändå. Avbrytning kan också ske samtidigt som en operation redan pågår. Därför förlorar en hanterare som bara återupptar en valfri continuation avbrytningen om den valfria variabeln ännu inte har fått något värde. withTaskCancellationHandler(operation:onCancel:isolation:)
Spärren behöver alltså hålla reda på mer än om det för tillfället finns en continuation. Att continuationen saknas kan betyda att installationen inte har skett, eller att ett sluttillstånd redan har valts. Dessa fall kräver olika hantering.
| Skyddat tillstånd | Installera continuationen | Ta emot slutförande eller avbrytning |
|---|---|---|
| Tomt | Spara continuationen | Spara sluttillståndet |
| Väntar | Avvisa en andra installation | Ta bort continuationen och spara sluttillståndet |
| Avslutat | Återuppta den nyinstallerade continuationen med det sparade resultatet | Ignorera den senare terminala händelsen |
Tabellen är också ett hjälpmedel vid felsökning. Om en implementation inte kan representera ett sluttillstånd innan installationen, granska vägen för en redan avbruten uppgift. Om två terminala händelser kan hämta continuationen, kontrollera om avläsning och borttagning sker i samma kritiska sektion.
Bygg ett lokalt exempel på din Mac
Använd en Swift 6-verktygskedja med macOS SDK. Kontrollera den valda verktygskedjan i Terminal med swift --version. Programmet nedan använder endast Foundation och Dispatch; det behöver inga paketberoenden. Swifts guide till kommandoradsverktyg beskriver hur du kontrollerar installationen och använder en asynkron @main-ingångspunkt. Bygg ett kommandoradsverktyg
Spara följande tre Swift-block i angiven ordning i en enda fil med namnet ContinuationRace.swift. Tillsammans utgör de ett komplett program. Det gick inte att använda Swift i forskningsmiljön, så programmet granskades men kompilerades eller kördes inte här.
Implementera den synkroniserade spärren
Apple dokumenterar NSLock som ett sätt att skydda en kritisk sektion. Låsning och upplåsning måste ske på samma tråd, och låset är inte rekursivt. Här hålls varje kritisk sektion inom en synkron metod, innehåller ingen pauspunkt och avslutas innan continuationen återupptas. 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()))
}
}
Spärren använder @unchecked Sendable eftersom dess föränderliga lagring skyddas manuellt. Den annoteringen överför ansvaret till implementationen; den tillför ingen synkronisering. Båda föränderliga egenskaperna förblir privata, och all åtkomst sker under samma lås. Apple beskriver uttryckligen låsskyddat tillstånd som ett sätt att uppfylla de semantiska kraven för unchecked sendability. Sendable
Lägg märke till att finish ändrar tillståndet innan låset släpps. En konkurrerande terminal händelse kan då bara se finished, även om den vinnande tråden ännu inte har anropat resume. Vinnaren behåller den uttagna continuationen lokalt; inget konkurrerande anrop kan ta ut den igen.
Koppla slutförande och avbrytning till samma spärr
Callback-beräkningen använder en global dispatch-kö. Apple dokumenterar att arbete som skickas in asynkront fortsätter separat från den insändande koden och att bakgrundsarbete i Dispatch inte har någon garanterad trådidentitet. Därför passar trådoberoende samordning här. 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()
}
}
Skapa spärren innan avbrytningshanterarens omfattning börjar. Då kan en redan avbruten uppgift omedelbart registrera avbrytningen. När continuationen installeras därefter återupptar installationen den med det sparade felet och returnerar false, så adaptern hoppar över att starta beräkningen.
Avbrytning kan ändå ske efter att installationen har returnerat true och innan startDoubling anropas. I den ordningen startar och slutförs beräkningen, men resultatet ignoreras eftersom avbrytningen redan vann. Wrappern avbryter väntan; den lovar inte att stoppa den här lilla, begränsade beräkningen.
Den continuation-brygga som stöds är withCheckedThrowingContinuation; dess closure får continuationen som används för att returnera ett värde eller kasta ett fel. Exemplet anger continuationens heltalsresultat och feltyper uttryckligen för att undvika oklar typinferens. withCheckedThrowingContinuation(isolation:function:_:)
Kontrollera händelseordningen utan att förlita dig på väntetider
En tidsfördröjning avgör inte vilken händelse som vann en kapplöpning. Kontrollerna nedan går i stället igenom alla sex ordningar för installation, slutförande och avbrytning direkt genom spärren. Varje sekvens får också extra terminala händelser för att kontrollera att det valda resultatet förblir oförändrat.
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.")
}
}
Kompilera och kör från katalogen som innehåller filen:
swiftc -swift-version 6 -parse-as-library ContinuationRace.swift -o ContinuationRace
./ContinuationRace
Det förväntade resultatet vid framgång är Event-order checks and integration checks passed. De uttryckliga ordningskontrollerna validerar tillståndsövergångarna. Integrationsloopen kör den riktiga adaptern, men garanterar inte att en viss schemaläggningsordning inträffar och bevisar inte att varje samtidig körning har testats.
Varför återupptar implementationen exakt en gång?
För att säkerställa att continuationen återupptas högst en gång kan du betrakta det första lyckade anropet till finish. Det ersätter atomärt det aktuella tillståndet med finished. Om en continuation väntar är det bara det anropet som hämtar den. Varje senare anrop till finish returnerar utan att återuppta något. Om det ännu inte finns någon continuation väntar resultatet på den enda installationen.
För att säkerställa att den så småningom återupptas finns det två vägar. Antingen sker installationen först och den första terminala händelsen återupptar den väntande continuationen, eller så sker en terminal händelse först och installationen återupptar den med det sparade resultatet. Resonemanget förutsätter att installationen sker en gång och att minst en terminal händelse inträffar. En generell callback-källa som aldrig slutförs, i kombination med en uppgift som aldrig avbryts, skulle fortfarande lämna anroparen väntande.
Exemplets callback utför en begränsad beräkning och anropar slutförande vid både framgång och indatafel. Dubbla terminala callbacks ignoreras avsiktligt efter att en har vunnit. Det passar det angivna kontraktet med ett enda resultat; en ström med meningsfulla värden skulle kräva ett annat gränssnitt.
Vilka genvägar bryter mot kontraktet?
Att kontrollera en valfri continuation och sedan sätta den till nil räcker inte om åtgärderna inte synkroniseras. Två samtidiga anropare kan båda läsa samma continuation som inte är nil innan någon av dem tar bort den. Kontrollen, uttaget och ändringen av sluttillståndet måste vara en enda skyddad åtgärd.
En avbrytningskontroll innan continuationen skapas lämnar också ett glapp: avbrytning kan ske omedelbart efter kontrollen. Hanteraren och det sparade sluttillståndet täpper till glappet. Swifts språkguide beskriver avbrytning som kooperativ och anger avbrytningshanterare som mekanismen för omedelbar avisering. Concurrency — The Swift Programming Language
Att återuppta medan det delade låset hålls skapar ytterligare ett problem. Apple varnar särskilt för att avbrytningshanterare kan köras medan interna uppgiftslås hålls och att återupptagning av en continuation kan försöka ta dessa lås. Därför låser exemplet upp före varje anrop till resume. Avbrytningshanterare och lås
Slutligen skulle det ändra exempelts utåtriktade kontrakt att lägga till Task.checkCancellation() efter att continuationen har returnerat: avbrytning skulle kunna ersätta ett värde även efter att slutförandet vunnit spärren. Bestäm den policyn medvetet. För kontraktet som implementeras här använder installation, slutförande och avbrytning samma tillståndsmaskin, och dess första terminala övergång avgör anroparens resultat.
Källor
- Kontrollerad continuation — Apple.
- withTaskCancellationHandler(operation:onCancel:isolation:) — Apple.
- cancel() — Apple.
- NSLock — Apple.
- Sendable — Apple.
- DispatchQueue — Apple.
- withCheckedThrowingContinuation(isolation:function:_:) — Apple.
- Bygg ett kommandoradsverktyg — Swift.org.
- Concurrency — The Swift Programming Language — Swift-projektet.