För att köra CPU-intensiva Swift-beräkningar parallellt ger du arbetaren en uttrycklig @concurrent-gräns och delar upp oberoende arbete mellan underordnade uppgifter. Det här Mac-exemplet håller gränssnittsuppdateringar på MainActor och kombinerar syntetiska resultat utan delad mutering.
Varför tillägg av uppgifter ändå kan serialisera beräkningen
Anta att en Mac-app har en Calculate-knapp som startar en omfattande lokal beräkning. Om du omsluter beräkningen i Task { ... } får du en asynkron startpunkt, men en uppgift som skapas i en main-actor-kontext kan ärva den isoleringen. En lång synkron loop inuti uppgiften upptar fortfarande main actor. Apple visar den här skillnaden i Att anamma Swift-concurrency.
Att lägga till en uppgiftsgrupp lämnar också en viktig fråga obesvarad: var körs funktionen som innehåller den kostsamma loopen? Om varje underordnad uppgift anropar samma beräkningsmetod med @MainActor serialiserar den aktören beräkningen. Aktörer tillåter en uppgift i taget att komma åt sitt isolerade tillstånd, vilket förklaras i Swift-språkhandledningens kapitel om concurrency.
För Swift 6.2 och senare markerar du den asynkrona startpunkten för beräkningen med @concurrent. Det flyttar uttryckligen körningen bort från anroparens aktör. När funktionen NonisolatedNonsendingByDefault är aktiverad körs i stället en vanlig asynkron funktion utan isolering på anroparens aktör; det tidigare beteendet var annorlunda. Det implementerade förslaget SE-0461 beskriver både funktionens gräns och det uttryckliga attributet.
Två separata val ger det önskade beteendet: arbetarens isolering tillåter körning utanför MainActor, och flera underordnade uppgifter ger arbete som kan schemaläggas oberoende. Körtiden avgör vilka tillgängliga trådar som kör uppgifterna. Varken antalet uppgifter eller @concurrent utlovar ett visst antal kärnor som kör samtidigt. Apple beskriver den schemaläggningsmodellen i Att anamma Swift-concurrency.
Definiera en beräkning som kan delas upp korrekt
Exemplet nedan låter en Mac-användare starta eller avbryta en syntetisk beräkning och visa dess slutliga kontrollsumma. Alla indata är konstanter eller genererade heltal. Beräkningen har följande exakta värdekontrakt:
- Besök varje heltalsindex i
0..<countexakt en gång. - Konvertera varje index till
UInt64och tillämpax = (x &* 1_664_525) &+ 1_013_904_223upprepade gånger det begärda antalet varv. - Addera de resulterande värdena med omslagande addition.
- Returnera noll för tom indata, avvisa negativa värden för antal eller varv och avvisa en icke-positiv arbetargräns.
- Kasta ett fel när avbrott upptäcks i stället för att visa en ofullständig kontrollsumma som ett färdigt resultat.
Omslagande aritmetik är avsiktlig här. Swifts vanliga heltalsaritmetik utlöser ett fel vid spill; operatorerna &+ och &* hanterar uttryckligen spill. Se Avancerade operatorer.
Enligt det här kontraktet är varje index oberoende, och omslagande addition kombinerar delsummor modulo 2 upphöjt till 64. Därför bevaras det exakta slutvärdet när grupperingen eller slutförandeordningen ändras. Den slutsatsen följer av den valda aritmetiken. En flyttalsberäkning behöver en egen korrekthetsregel eftersom omgruppering av additioner kan ändra avrundningen.
Uppdelningen använder division och rest: varje arbetare får lika många index i grundfördelningen, och de första arbetarna får ett extra index var. Om antalet till exempel är 17 och det finns tre arbetare blir intervallen 0..<6, 6..<12 och 12..<17. De täcker indata exakt en gång, även resten.
Bygg det fullständiga beräkningsexemplet för Mac
Använd en verktygskedja för Swift 6.2 eller senare och ett macOS SwiftUI-appmål med macOS 13 eller senare som distributionsmål. Ersätt den genererade appens startpunkt och innehållsvy med följande två block, placerade tillsammans i en Swift-fil. De bildar ett komplett program; behåll bara en @main-appdeklaration i målet.
Beräkningsnamnrymden är uttryckligen nonisolated, så dess medlemmar får ingen härledd global aktörisolering. Swift dokumenterar den här typdeklarationen i SE-0449. Båda de asynkrona beräkningsmetoderna använder @concurrent för att uttryckligen ange sin körningsgräns.
import Foundation
import SwiftUI
nonisolated enum CalculationError: Error {
case invalidInput
}
nonisolated enum SyntheticCalculator {
@concurrent
static func run(
count: Int,
rounds: Int,
workerLimit: Int
) async throws -> UInt64 {
guard count >= 0, rounds >= 0, workerLimit > 0 else {
throw CalculationError.invalidInput
}
try Task.checkCancellation()
guard count > 0 else { return 0 }
let workers = min(count, workerLimit)
let baseSize = count / workers
let remainder = count % workers
return try await withThrowingTaskGroup(
of: UInt64.self,
returning: UInt64.self
) { group in
var lower = 0
for worker in 0..<workers {
try Task.checkCancellation()
let size = baseSize + (worker < remainder ? 1 : 0)
let start = lower
let end = start + size
lower = end
group.addTask {
try await calculateChunk(
lower: start,
upper: end,
rounds: rounds
)
}
}
var total: UInt64 = 0
for try await partial in group {
total = total &+ partial
}
try Task.checkCancellation()
return total
}
}
@concurrent
private static func calculateChunk(
lower: Int,
upper: Int,
rounds: Int
) async throws -> UInt64 {
try Task.checkCancellation()
var total: UInt64 = 0
for index in lower..<upper {
if index % 256 == 0 {
try Task.checkCancellation()
}
var value = UInt64(index)
for round in 0..<rounds {
if round % 4096 == 0 {
try Task.checkCancellation()
}
value = (value &* 1_664_525) &+ 1_013_904_223
}
total = total &+ value
}
try Task.checkCancellation()
return total
}
}
Varje underordnad uppgift hämtar oföränderliga gränser och returnerar ett heltal. Endast den överordnade uppgiften uppdaterar total; underordnade uppgifter lägger aldrig till i en delad samling eller ändrar gränssnittstillstånd. Uppgiftsgrupper stöder samtidig körning av underordnade uppgifter, kräver sendable-resultat och håller de underordnade uppgifternas livstid inom gruppens omfång. Se Apples dokumentation om TaskGroup.
Lägg till följande gränssnittskod i samma fil:
@main
@MainActor
struct ParallelCalculationApp: App {
var body: some Scene {
WindowGroup {
CalculationView()
}
}
}
@MainActor
struct CalculationView: View {
@State private var status = "Ready"
@State private var work: Task<Void, Never>?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Synthetic Calculation")
.font(.title2)
Text(status)
.textSelection(.enabled)
HStack {
Button("Calculate") {
startCalculation()
}
.disabled(work != nil)
Button("Cancel") {
work?.cancel()
}
.disabled(work == nil)
}
}
.padding(24)
.frame(minWidth: 440, minHeight: 180)
.onDisappear {
work?.cancel()
}
}
private func startCalculation() {
guard work == nil else { return }
status = "Calculating…"
let workerLimit = max(
1,
ProcessInfo.processInfo.activeProcessorCount
)
work = Task { @MainActor in
defer { work = nil }
do {
let checksum = try await SyntheticCalculator.run(
count: 1_000_000,
rounds: 128,
workerLimit: workerLimit
)
try Task.checkCancellation()
status = "Checksum: \(checksum)"
} catch is CancellationError {
status = "Cancelled"
} catch {
status = "Calculation failed: \(error)"
}
}
}
}
Gränssnittet äger den yttre uppgiftshandtaget så att knappen Cancel kan avbryta uppgiften. Uppgiften förblir isolerad till main actor, anropar den samtidiga beräkningen och uppdaterar den visade statusen efter att resultatet har inväntats. Om vyn försvinner går det också att avbryta uppgiften tack vare handtaget. Apples dokumentation om Task förklarar att det inte avbryter uppgiften att slänga handtaget.
Arbetargränsen utgår från Foundations activeProcessorCount, som rapporterar antalet aktiva processorkärnor som är tillgängliga på datorn. Se detta som ett första val av storlek att mäta, inte som ett garanterat optimalt antal uppgifter. Den dokumenterade betydelsen finns under activeProcessorCount.
Kontrollera korrektheten innan du jämför prestanda
Börja med det lilla exempelindatat: ändra anropet till count: 17, rounds: 4 och workerLimit: 3. Den förväntade kontrollsumman är 18149427060254280572. Upprepa med workerLimit: 1; värdet måste stämma. Kontrollera också count: 0, som ska returnera noll, och ett positivt antal som är mindre än arbetargränsen, vilket ändå ska omfatta varje index.
Exempelkontrollsumman och intervallens täckning kontrollerades med en liten Python-återskapning av den omslagande aritmetiken. Ingen Swift-kompilator fanns tillgänglig i researchmiljön, så Swift-programmet har inte kompilerats eller körts här.
Återställ sedan det större indatat och jämför optimerade byggen med arbetargränserna ett, två och antalet aktiva processorkärnor. Behåll samma antal och varv, spara den resulterande kontrollsumman och mät flera körningar. Detta är ett föreslaget mätförfarande; inga tider eller prestandavinster görs gällande.
En körning med en arbetare använder samma samtidiga gräns och uppgiftsgruppsstruktur, vilket gör den till en användbar jämförelse för den här implementationen. Små arbetsmängder kanske inte motiverar kostnaden för uppdelning och schemaläggning. Om en realistisk beräkning ger olika index avsevärt olika arbetskostnad kan intervall med lika många index också innebära att en arbetare blir klar senare; mindre delar med ett begränsat antal pågående uppgifter är ett rimligt nästa experiment.
För en beräkning som skapar en array kopplar du ett delindex till varje resultat och sätter ihop delarna i indataordning. Underordnade uppgifter i en uppgiftsgrupp kan köras i valfri ordning, och gruppen visar slutförda resultat allteftersom de blir tillgängliga. Kontrollsumman slipper ordningsproblem enbart eftersom dess valda reduktion är oberoende av ordningen. Se TaskGroup.
Gör avbrott och isoleringsfel synliga
Avbrott sker samarbetsmässigt. Anropet cancel() markerar uppgiften; beräkningskoden måste upptäcka det tillståndet. Kontrollerna i båda looparna ger arbetaren möjlighet att kasta ett fel, även när ett enskilt index har många varv. Dessa intervall är implementeringsval, inte en utlovad avbrottstid. Apple beskriver det här ansvaret i Task.
Loopen som läser kastande resultat spelar också roll. Om en underordnad uppgift kastar ett fel och felet fortplantas ut ur gruppens kropp avbryts de återstående underordnade uppgifterna. Gruppen väntar ändå på att de ska bli klara, så deras avbrottskontroller är fortfarande nödvändiga. Swifts standardbiblioteksimplementation dokumenterar det här beteendet i TaskGroup.swift.
Om gränssnittet fortfarande hackar ska du följa hela den kostsamma anropskedjan. Leta efter en arbetare som anropar en metod på main actor, använder aktörisolerade egenskaper eller utför omfattande förberedelser innan den når den samtidiga gränsen. Skicka de nödvändiga heltalsvärdena till arbetaren och behåll de långa looparna inom den gränsen.
Om kompilatorn avvisar @concurrent ska du kontrollera verktygskedjans version. Attributet implementerades i Swift 6.2 och gäller asynkrona funktioner. För en äldre verktygskedja beskrivs den tidigare modellen för asynkron körning utan isolering i SE-0461; använd dokumentation som stämmer med kompilatorn i stället för att anta att alla versioner ger nonisolated async samma beteende.
För den här Mac-uppgiften är arkitekturen enkel: main actor äger gränssnittstillståndet, samtidiga arbetare tar emot oberoende värden och den överordnade uppgiften kombinerar fullständiga resultat. Bevara värdekontraktet när du justerar delarnas storlek och kontrollera både att kontrollsumman är densamma och att gränssnittet fortfarande är responsivt innan du behåller en prestandaändring.
Källor
- Att anamma Swift-concurrency — Apple.
- Kör funktioner med nonisolated async på anroparens aktör som standard — Swift.org, Swift Evolution.
- Tillåt nonisolated för att förhindra global aktörinferens — Swift.org, Swift Evolution.
- Concurrency — Swift.org, programmeringsspråket Swift.
- Avancerade operatorer — Swift.org, programmeringsspråket Swift.
- Task — Apples utvecklardokumentation.
- TaskGroup — Apples utvecklardokumentation.
- activeProcessorCount — Apples utvecklardokumentation.
- TaskGroup.swift — Swift.org, Swifts standardbibliotek.