Använd en Swift-uppgiftsgrupp för att samla konkurrerande beräkningar i den ordning de slutförs, hoppa över nil och avbryt de återstående underuppgifterna när ett värde kommer. Det här kommandoradsexemplet för Mac hittar en användbar rektangulär layout för syntetiska brickor och förklarar när resultatet faktiskt kan returneras.
Definiera vad som räknas som det första användbara resultatet
Anta att ett litet layoutverktyg för skrivbordet behöver ordna 221 syntetiska brickor i en rektangel. Flera lokala beräkningar söker igenom olika möjliga radantal. Vilken icke-trivial faktor som helst godtas: 13 rader och 17 kolumner fungerar, liksom 17 rader och 13 kolumner. Uppgiften för läsaren är att godta den giltiga beräkning som slutförs först.
Det här är ett kontrakt baserat på slutförandeordning. Det väljer inte den minsta faktorn, den mest balanserade rektangeln eller den beräkning som står först i listan. Apple dokumenterar att successiva resultat från en uppgiftsgrupp kommer i slutförandeordning, oberoende av insättningsordningen. TaskGroup.next(isolation:)
Definiera beteendet innan du skriver hjälpfunktionen:
| Situation | Beteende som krävs |
|---|---|
En beräkning slutförs med nil |
Fortsätt vänta på andra beräkningar. |
| En beräkning slutförs med ett värde | Välj det och begär att det återstående arbetet avbryts. |
Alla beräkningar returnerar nil |
Returnera nil. |
| Beräkningslistan är tom | Returnera nil. |
| Förälderns avbrytning upptäcks | Sluta välja resultat och returnera nil. |
Den sista raden är ett medvetet API-val i det här exemplet. Här betyder nil att anropet inte ger något användbart värde; det skiljer inte mellan en misslyckad sökning och en avbrytning. Om anroparen behöver den skillnaden kan du använda en uttrycklig utfallstyp eller ett kontrakt som kan kasta fel.
Bygg ett komplett lokalt exempel på din Mac
Använd en verktygskedja för Swift 6 eller senare. Kontrollera den i Terminal med swift --version. Om Swift inte är tillgängligt följer du de officiella instruktionerna Install Swift — macOS. Programmet använder Foundation och Swifts API:er för samtidighet utan externa paketberoenden.
Skapa ett nytt körbart paket med dessa kommandon. Det följer Swifts dokumenterade arbetsflöde för att skapa och köra paket. Build a Command-line Tool
mkdir FirstNonNilDemo
cd FirstNonNilDemo
swift package init --name FirstNonNilDemo --type executable
Ersätt den genererade källfilen för det körbara programmet med hela programmet nedan. Beroende på den genererade strukturen kan filen heta Sources/FirstNonNilDemo/FirstNonNilDemo.swift eller Sources/main.swift. Använd en enda startpunktsfil med namnet FirstNonNilDemo.swift; om mallen skapade main.swift byter du namn på den filen innan du lägger till programmet med @main. Swifts guide dokumenterar att ett program använder antingen main.swift eller en startpunkt med @main. Build a Command-line Tool
import Foundation
func firstNonNil<Value: Sendable>(
_ calculations: [@Sendable () async -> Value?]
) async -> Value? {
guard !Task.isCancelled else { return nil }
return await withTaskGroup(
of: Value?.self,
returning: Value?.self
) { group in
defer { group.cancelAll() }
for calculation in calculations {
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else { return nil }
return await calculation()
}
guard added else { break }
}
for await candidate in group {
guard !Task.isCancelled else { return nil }
if let value = candidate {
return value
}
}
return nil
}
}
func findFactor(total: Int, range: ClosedRange<Int>) -> Int? {
guard total > 1 else { return nil }
for rows in range {
guard !Task.isCancelled else { return nil }
guard rows > 1, rows < total else { continue }
if total % rows == 0 {
return rows
}
}
return nil
}
@main
struct FirstNonNilDemo {
static func main() async {
let checks: [@Sendable () async -> Int?] = [
{ nil },
{ 42 }
]
let checkResult = await firstNonNil(checks)
precondition(checkResult == 42)
let empty: [@Sendable () async -> Int?] = []
let emptyResult = await firstNonNil(empty)
precondition(emptyResult == nil)
for rawText in [" 221\n", " 223 ", "tiles"] {
let text = rawText.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard let total = Int(text), total > 1 else {
print("Input must be a whole number greater than 1.")
continue
}
let calculations: [@Sendable () async -> Int?] = [
{ findFactor(total: total, range: 2...5) },
{ findFactor(total: total, range: 6...16) },
{ findFactor(total: total, range: 17...30) }
]
if let rows = await firstNonNil(calculations) {
print("\(total) tiles: \(rows) rows x \(total / rows) columns")
} else {
print("\(total) tiles: no factor in the supplied ranges.")
}
}
}
}
Kör paketet från dess katalog:
swift run FirstNonNilDemo
För 221 är antingen 221 tiles: 13 rows x 17 columns eller 221 tiles: 17 rows x 13 columns godtagbart. För 223 rapporterar programmet att det saknas en faktor inom de angivna intervallen. Texten tiles ger inmatningsmeddelandet. Det här är förväntade resultat, inte en dokumenterad körningstranskription. Aritmetiken kontrollerades separat; en Swift-kompilator var inte tillgänglig i forskningsmiljön, så Swift-programmet granskades men kördes inte.
Foundation hanterar endast rensning av omgivande blanksteg. Dess metod trimmingCharacters(in:) tar bort matchande tecken från båda ändarna; den tar inte bort tecken inuti strängen. Därför blir 221 till 221, medan 2 21 fortfarande är ogiltigt för det här programmet. trimmingCharacters(in:)
Därför hoppar hjälpfunktionen över nil utan att förlora senare resultat
Gruppens typ för underuppgiftsresultat är Value?. Varje underuppgift lämnar därför antingen ett användbart värde eller ett vanligt tomt resultat. for await-loopen tar emot slutförda underuppgiftsresultat, och dess inre valfria bindning väljer bara en kandidat som inte är nil.
Om du använder group.next() direkt finns det två olika sorters tomhet. Den dokumenterade returtypen är ChildTaskResult?. Med valfria underuppgiftsresultat blir typen Value??: den yttre valfria typen anger om det finns ytterligare ett underuppgiftsresultat, och den inre valfria typen anger om underuppgiften hittade ett värde. TaskGroup.next(isolation:)
Den här skillnaden förklarar ett vanligt misstag: att ta ett enda resultat och returnera direkt. Om den första slutförda beräkningen returnerar nil kan andra underuppgifter fortfarande ha användbara svar. Loopen i det fullständiga exemplet fortsätter att ta emot resultat tills den hittar ett värde eller gruppen är uttömd.
Hjälpfunktionen använder addTaskUnlessCancelled och kontrollerar dess booleska returvärde. Apple dokumenterar att den här metoden avstår från att skapa en underuppgift om gruppen redan har avbrutits. Genom att bryta inlämningsloopen undviker du att fortsätta lägga till arbete efter att det villkoret har upptäckts. addTaskUnlessCancelled(priority:operation:)
Beräkningarna fångar det oföränderliga antalet brickor och returnerar sina svar via gruppen. De uppdaterar inte en delad variabel för vinnaren. Det generiska resultatet måste uppfylla kravet på Sendable, i enlighet med den dokumenterade begränsningen för uppgiftsgrupper. withTaskGroup(of:returning:isolation:body:)
Därför innebär det inte att du kan returnera direkt när du valt en vinnare
Att returnera ett värde från gruppens closure väljer svaret, men den omslutande withTaskGroup väntar ändå på att alla underuppgifter ska slutföras. Apple dokumenterar uttryckligen att även avbrutna underuppgifter måste slutföras innan funktionen returnerar. withTaskGroup(of:returning:isolation:body:)
Blocket defer anropar group.cancelAll() varje gång dess closure avslutas. Det begär att återstående beräkningar avbryts. Avbrytning avbryter inte godtyckliga beräkningar: underuppgifterna måste samarbeta och själva avsluta arbetet i förtid. TaskGroup
I det här exemplet kontrollerar findFactor Task.isCancelled före varje möjlig divisor. Varje arbetsenhet är liten, så det finns ofta tillfälle att avbryta. För en större beräkning placerar du kontroller mellan meningsfulla, avgränsade arbetsmoment. En kontroll enbart före en lång, odelbar operation innebär att operationen fortsätter tills den är klar. Swifts språkguide beskriver avbrytning som samarbetsbaserad och anger att det är ett av de sätt som stöds att returnera nil. Concurrency
Om en konkurrerande beräkning aldrig slutförs och aldrig reagerar på avbrytning kan den här strukturerade hjälpfunktionen inte lova att returnera snabbt. Den fungerande lösningen är att göra varje arbetare tidsbegränsad eller avbrytningsmedveten. Att lägga till ett konkurrerande timeoutresultat tar inte bort gruppens skyldighet att invänta sina underuppgifter.
Förälderns avbrytning kontrolleras före inlämning, när underuppgiften startar, inuti aritmetikloopen och innan varje resultat godtas. Dessa kontroller upptäcker avbrytning vid vissa tidpunkter; de skapar ingen atomisk regel som alltid låter avbrytning åsidosätta ett värde. En avbrytning som kommer efter den sista kontrollen kan sammanfalla med en lyckad retur.
Välj rätt urvalsregel och kontrollera dess gränsfall
Det syntetiska exemplet ger ett användbart beslutstest: både 13 och 17 uppfyller det angivna kontraktet. Om verktyget kräver det lägsta radantalet är det fel att godta vilket som helst av resultaten. Slutförandeordning är bara användbar när alla godtagbara svar är likvärdiga för läsarens uppgift.
| Vad anroparen behöver | Lämpligt tillvägagångssätt |
|---|---|
| Vilket godtagbart värde som helst när beräkningarna slutförs | Använd uppgiftsgruppsloopen ovan. |
| Den första lyckade beräkningen i en fast prioritetslista | Utvärdera i prioritetsordning, eller behåll index och avgör vilka tidigare kandidater som måste slutföras innan du väljer. |
| Det minsta eller bästa resultatet enligt en poängsättning | Jämför de relevanta resultaten innan du väljer. |
| Flera kända resultat som behövs tillsammans | Överväg async let. |
Swifts språkguide visar async let för kända underuppgifter vars resultat behövs senare, och uppgiftsgrupper för uttrycklig kontroll över dynamiskt tillagda underuppgifter. Att invänta namngivna resultat i en vald ordning implementerar inte en urvalsloop baserad på slutförandeordning. Concurrency
Använd dessa kontroller när du anpassar programmet:
- Behåll påståendet om en tom lista: den ska returnera
nil. - Behåll påståendet med både
niloch42: det ska returnera42oavsett slutförandeordning. - Testa beräkningar som alla returnerar
nil. - Om det finns flera giltiga svar ska du kontrollera att resultatet ingår i den giltiga mängden i stället för att kräva en viss vinnare.
- Granska varje arbetare för att säkerställa avgränsade arbetsförlopp och avbrytningskontroller.
- Begränsa rapporteringen av en misslyckad sökning till de angivna intervallen; dra inte slutsatsen att det inte finns något svar någon annanstans.
De tre små faktorsökningarna demonstrerar beteendet, inte någon prestandaförbättring. En sekventiell sökning räcker för dessa exempelvärden. Använd mönstret med konkurrerande beräkningar när den faktiska uppgiften tjänar på att godta likvärdiga resultat, och mät arbetsbelastningen innan du bestämmer hur mycket arbete som ska köras samtidigt.
Håll godkännanderegeln tydlig, hoppa över tomma underuppgiftsresultat och se till att det återstående arbetet kan avslutas. Då får Mac-verktyget ett förutsägbart värdekontrakt: det första användbara resultatet som gruppen levererar, följt av att underuppgifterna avslutar sin samarbetsbaserade nedstängning.
Källor
- Install Swift — macOS — Swift.org
- Build a Command-line Tool — Swift.org
- TaskGroup — Apple
- withTaskGroup(of:returning:isolation:body:) — Apple
- next(isolation:) — Apple
- addTaskUnlessCancelled(priority:operation:) — Apple
- trimmingCharacters(in:) — Apple
- Concurrency — The Swift Programming Language, Swift.org