För en Mac-app som måste slutföra lokala asynkrona beräkningar i inlämningsordning, använd en synkron main-actor-inlämningsmetod som kedjar varje uppgift bakom sin föregångare. Aktörisolering ensam ger inte den ordningsgarantin.
Den här genomgången bygger en liten kö för syntetiska heltalberäkningar. Den definierar exakt när inlämning sker, bevarar ordning över avbrottspunkter, returnerar varje beräknings resultat och låter senare jobb fortsätta efter att ett tidigare jobb misslyckats.
Vad betyder strikt inlämningsordning?
Anta att en stationär kalkylator lämnar in jobb A, B och C. Det krävda kontraktet är: A:s beräkning slutförs innan B:s beräkning börjar, och B slutförs innan C börjar. Att vänta på tre oberoende startade uppgifter i alfabetisk ordning skulle bara ordna observationerna av deras resultat.
Inlämningsgränsen spelar roll. I denna implementation sker inlämning när den main-actor-isolerade submit metoden körs och installerar nästa länk i kedjan. Konsekutiva synkrona anrop från en main-actor-kontext etablerar A, B, C-ordning utan att avbryta mellan dem.
Om oberoende uppgifter begär inlämning samtidigt är deras ankomstordning ospecificerad. Kön bevarar den ordning som dess inlämningsmetod accepterar dem. Om ditt program kräver en särskild ordning bland dessa producenter, etablera den ordningen innan inlämning – till exempel genom att samla en ändlig batch i en explicit ordnad array.
Designen använder main-actor för det korta bokföringssteget. Apple definierar MainActor som en singleton-aktör vars exekutor är ekvivalent med huvuddispatch-kön. Den faktiska beräkningen nedan tillhör en separat arbetaraktör. MainActor-dokumentation
Varför räcker inte en aktör eller en seriell operationskö?
En aktör kör aktör-isolerat arbete ett stycke i taget, men en async-metod kan avbryta och tillåta att ett annat anrop fortsätter. Swifts aktördesign anger explicit att uppgifter som väntar på en aktör inte garanteras att köras i den ordning de ursprungligen väntade på den. Därför etablerar inte att lägga en hel asynkron beräkning inne i en aktör självt finish-before-next-start-beteende. SE-0306: Actors
Att skapa separata Task-instanser ger inte heller något beroende mellan dem. Uppgifter kan börja köra efter skapande; att behålla deras handtag låter dig awaita eller avbryta dem, men etablerar ingen exekveringssekvens. Task-dokumentation
Foundation erbjuder en annan ordningsmekanism. OperationQueue schemalägger enligt beredskap, prioritet och beroenden; Apple råder till att använda beroenden när en specifik exekveringsordning betyder något. Att sätta maxConcurrentOperationCount till 1 begränsar samtidighet, men ordningskravet kräver fortfarande explicita beroenden. OperationQueue-dokumentation
Välj mekanism enligt vad ditt program redan behöver:
| Situation | Lämplig metod | Vad etablerar ordning? |
|---|---|---|
| En känd batch i en async-funktion | En loop som awaitar varje beräkning innan den fortsätter | Sekventiellt kontrollflöde |
| Jobb anländer över tid från skrivbordsåtgärder | En synkron inlämningsmetod med en uppgiftskedja | Varje beräkning awaitar sin föregångares färdigställelseport |
| Befintliga Foundation-operationer | Beroenden konfigurerade innan köning | Varje operation beror på föregående operations färdigställandestatus |
Uppgiftskedjan är användbar här eftersom inlämning returnerar omedelbart med ett handtag medan beräkningar körs senare. För en fast batch utan anländande jobb är den sekventiella loopen oftast mindre kod.
Bygg en kö som väntar på fullständiga asynkrona beräkningar
Kön lagrar en privat, icke-kastande färdigställandeuppgift som kallas tail. Varje inlämning fångar den nuvarande svansen, skapar en beräkningsuppgift som väntar på den, och ersätter svansen med en ny uppgift som observerar beräkningsens slutförande.
Färdigställandeuppgiften awaitar result, vilket representerar antingen framgång eller misslyckande efter att en uppgift slutförts. Det tillåter en misslyckad beräkning att släppa nästa jobb utan att kasta bort felet från beräkningsens egna handtag. Anropar kan fortfarande hämta felet genom handtaget. Task result-dokumentation
Följande är en komplett kommandoradsdemonstration för en Mac med en Swift 6-toolchain. Spara den som OrderedDemo.swift. Källkoden granskades men kunde inte kompileras i denna forskningsmiljö eftersom ingen Swift-kompilator var tillgänglig.
import Foundation
@MainActor
final class OrderedIntQueue {
private var tail: Task<Void, Never>?
func submit(
_ operation: @escaping @Sendable () async throws -> Int
) -> Task<Int, Error> {
let predecessor = tail
let job = Task<Int, Error> {
if let predecessor {
await predecessor.value
}
try Task.checkCancellation()
return try await operation()
}
tail = Task<Void, Never> {
_ = await job.result
}
return job
}
}
enum CalculationError: Error {
case inputOutOfRange
}
actor CalculationWorker {
private var events: [String] = []
func square(_ input: Int, label: String) async throws -> Int {
events.append("start \(label)")
guard (-1000...1000).contains(input) else {
throw CalculationError.inputOutOfRange
}
await Task.yield()
try Task.checkCancellation()
let answer = input * input
events.append("finish \(label)")
return answer
}
func trace() -> [String] {
events
}
}
@main
struct OrderedDemo {
@MainActor
static func main() async {
let queue = OrderedIntQueue()
let worker = CalculationWorker()
let a = queue.submit {
try await worker.square(3, label: "A")
}
let b = queue.submit {
try await worker.square(4, label: "B")
}
let c = queue.submit {
try await worker.square(5, label: "C")
}
var answers: [Int] = []
for job in [a, b, c] {
switch await job.result {
case .success(let answer):
answers.append(answer)
case .failure(let error):
print("Calculation failed: \(error)")
}
}
let events = await worker.trace()
precondition(answers == [9, 16, 25])
precondition(events == [
"start A", "finish A",
"start B", "finish B",
"start C", "finish C"
])
print(events.joined(separator: "\n"))
print("Answers: \(answers)")
}
}
Kompilera och kör det sparade programmet från dess katalog i Terminal:
swiftc -swift-version 6 -parse-as-library OrderedDemo.swift -o OrderedDemo
./OrderedDemo
Förväntad utdata följer från exemplets indata och beroendekedja:
start A
finish A
start B
finish B
start C
finish C
Answers: [9, 16, 25]
Assertionerna kontrollerar både returnerade värden och beräkningsgränser. Att bara skriva ut resultat medan man awaitar handtag i inlämningsordning skulle inte etablera att beräkningar kördes i den ordningen.
Arbetaren anropar medvetet Task.yield() så att beräkningen innehåller en möjlighet att avbryta. Att ge efter tillåter andra uppgifter att exekveras, även om exekutören kan omedelbart återuppta samma uppgift. Köns ordning kommer alltså från sina beroenden, inte från en antagande om att yielding kommer att byta uppgifter. Task yield-dokumentation
Varför bevarar kedjan ordningen över avbrott?
Tre detaljer etablerar garantin. Först, submit innehåller ingen avbrottspunkt, så ett annat main-actor-inlämningsanrop kan inte infogas medan den fångar och ersätter tail. Swift dokumenterar att aktör-isolerad kod utan avbrottspunkter körs utan att en annan aktörsuppgift avbryter den sekvensen. Concurrency: Actors
Andra, varje jobb awaitar sin fångade föregångare innan det anropar beräkningsstängningen. Att komma åt en icke-kastande uppgifts value väntar på att den uppgiften slutförs. Det senare jobbet kan köra sin väntande bokföring tidigt, men kan inte börja sin beräkning tidigt. Nonthrowing Task value-dokumentation
Tredje, föregångarporten slutförs först när det tidigare jobbet producerar sitt resultat eller fel. Att följa länkarna bakåt etablerar sekvensen för varje accepterad inlämning: C väntar på B:s färdigställelseport, som väntar på B, som väntar på A:s färdigställelseport.
Detta är en ordningsgaranti för arbetet som awaitas inuti varje inlämnad stängning. Om en stängning startar en annan ostrukturerad uppgift och returnerar utan att awaita den, kan det extra arbetet överleva stängningen. Inkludera alla krävda beräkningssteg i den awaitade operationen innan du returnerar.
För skrivbordsintegration, håll en köinstans i main-actor-kontrollern eller modellen som hanterar de relevanta åtgärderna. Anropa submit direkt från den kontexten, och använd sedan det returnerade handtaget för att observera slutförande. Att skapa en oberoende uppgift först och inlägga den först efter att den börjat skulle flytta inlämningsgränsen till den uppgiftens eventuella exekvering.
Hur ska fel och avbryt bete sig?
Den här implementationen följer en continue-after-failure-policy. Ett misslyckat jobb behåller sitt fel, dess färdigställelseport slutförs, och nästa accepterade jobb blir berättigat att beräkna. Demonstrationen inspekterar varje handtags result snarare än att överge de återstående resultaten efter det första felet.
För en enkel felkontroll, ändra B:s indata från 4 till 1001. Arbetaren avvisar det före multiplikation. Uppdatera de förväntade svaren till [9, 25] och den förväntade spåren till start A, finish A, start B, start C, finish C. Programmet bör skriva ut felet och ändå beräkna C. Indatagränsen är ett illustrativt kontrakt valt för att hålla denna aritmetik liten.
Avbryt är kooperativ. Kön kontrollerar avbryt efter att ha väntat på sin föregångare, och arbetaren kontrollerar igen efter att ha gett efter. Task.checkCancellation() kastar CancellationError när avbryt har begärts. Task checkCancellation-dokumentation
För att kontrollera avbryt före exekvering, anropa b.cancel() omedelbart efter att ha inlämnat C och före resultatslingan. Med dessa oavbrutna main-actor-inlämningar kan B inte ha kommit in i sin beräkning än. Justera de förväntade svaren till [9, 25] och ta bort båda B-poster från den förväntade spåren. B väntar fortfarande på A:s färdigställelseport innan det rapporterar avbryt; C fortsätter sedan.
Håll färdigställelseportarna privata. De representerar kedjans bokföring, medan de returnerade handtagen representerar individuella beräkningar. Att avbryta ett jobb betyder inte att en föregående beräkning ska stoppa eller att senare beräkningar ska starta medan den fortfarande körs.
Om ditt program istället kräver att stoppa hela batchen efter ett fel, behövs en explicit batchpolicy. Att ändra porten till att propagera fel skulle ändra vilka senare jobb som exekveras och hur deras utfall måste rapporteras.
När ska du använda Foundation-beroenden istället?
Om skrivbordsprogrammet redan representerar arbete som Foundation Operation-objekt, etablera ett beroende från varje operation till dess föregångare före köning. Foundation anser beroendet uppfyllt när föregångaren slutförs, oavsett om det lyckades eller misslyckades; success-dependent-beteende behöver separat utfallshantering. Operation-dokumentation
Var försiktig när du överför Swift-async-funktioner in i den modellen. En synkron operation som startar en Task och omedelbart returnerar representerar inte uppgiftens fulla livslängd. En anpassad asynkron operation måste korrekt hantera sin exekvering och färdigställandestatus, inklusive de krävda notifieringarna, så att beroende operationer väntar på det avsedda arbetet. Apple dokumenterar dessa ansvarsområden under asynkrona operationer och färdigställanderapportering. Operation lifecycle-dokumentation
För den lokala asynkrona kalkylatorn som utvecklats här, håller uppgiftskedjan kontraktet synligt: acceptera inlämningar synkront, awaita föregångarens slutförande, köra den fullständiga beräkningen, och exponera dess utfall. Bevara dessa gränser när du kopplar kön till skrivbordsåtgärder, och validera exekvering med start-och-slut-spår samt returnerade värden.
Källor
- MainActor — Apple
- SE-0306: Actors — Swift
- Task — Apple
- OperationQueue — Apple
- result — Apple
- yield() — Apple
- Concurrency — Swift
- value: Nonthrowing task — Apple
- checkCancellation() — Apple
- Operation — Apple