Gebruik één langdurig bestaande Foundation OperationQueue en stel maxConcurrentOperationCount in voordat je berekeningen indient. Voor een Mac-hulpprogramma dat één verzoek per knopdruk ontvangt, begrenst dit het gelijktijdige werk terwijl latere verzoeken kunnen wachten.
Deze handleiding bouwt de berekeningsengine voor die desktoptaak en een zelfstandig programma om het gedrag ervan te controleren. Elk verzoek bevat synthetische gehele getallen en elk resultaat blijft in het geheugen. De berekening wordt synchroon binnen de bewerking uitgevoerd, zodat de levensduur van de bewerking het daadwerkelijke werk omvat.
Welke tool voor gelijktijdigheid is geschikt voor afzonderlijke binnenkomende verzoeken?
Een OperationQueue accepteert bewerkingen afzonderlijk, en de blokgebaseerde methode addOperation(_:) verpakt elke ingediende closure in een bewerking. Bewaar de wachtrij in een object dat gedurende meerdere verzoeken blijft bestaan en dien vervolgens voor elk binnenkomend item één closure in. Apple’s documentatie over addOperation(_:) beschrijft deze indieningsmethode.
Stel maxConcurrentOperationCount in op een positieve limiet, zoals 2. Apple definieert deze eigenschap als het maximumaantal bewerkingen van die wachtrij dat gelijktijdig kan worden uitgevoerd. De limiet geldt voor die specifieke wachtrij; voor elk verzoek een aparte wachtrij maken zou een gedeelde limiet tenietdoen. Als je de limiet later verlaagt, blijven al lopende bewerkingen ook lopen. Apple’s documentatie over gelijktijdigheidslimieten licht deze grenzen toe.
Een Swift-taakgroep heeft een andere levensduur. Die hoort binnen een scope withTaskGroup, kan niet worden gebruikt buiten de taak die haar heeft aangemaakt en wacht op de onderliggende taken voordat ze terugkeert. Een taakgroep opslaan als eigenschap voor toekomstige, niet-gerelateerde knopdrukken wordt daarom niet ondersteund. Voor deze synchrone berekeningsengine biedt een behouden operation queue de vereiste levensduur en een expliciete instelling voor gelijktijdigheid. De documentatie over taakgroepen in de Swift-standaardbibliotheek beschrijft deze beperkingen.
| Indeling van je werk | Geschikte aanpak |
|---|---|
| Onafhankelijke synchrone berekeningen komen afzonderlijk binnen | Behoud één OperationQueue en dien één bewerking per berekening in. |
| Een asynchrone bovenliggende bewerking beheert een eindige verzameling onderliggende taken | Gebruik een taakgroep binnen de scope van die bovenliggende bewerking; beheer het toelaten van taken expliciet als een numerieke limiet vereist is. |
| Werk moet de desktopinterface bijwerken | Voer de berekening apart uit en geef het resultaat daarna door aan code die geïsoleerd is tot MainActor. |
De laatste rij gaat over het doorgeven van resultaten. In Swift’s taalgids wordt de main actor aangewezen als plek voor interfacestatus en worden expliciet main-actor-geïsoleerde closures gedemonstreerd. Houd rekenintensief werk buiten die stap voor het doorgeven van resultaten. The Swift Programming Language: Concurrency licht isolatie tot de main actor toe.
Definieer de berekening en het contract voor voltooiing
Het voorbeeld accepteert een geheel getal als identificatie en een aantal iteraties van 1 tot en met 1_000_000. Bij een aantal iteraties van n telt het de kwadraten op van alle gehele getallen van 1 tot en met n. Het retourneert de identificatie samen met een resultaat van het type UInt64. De identificatie labelt de uitvoer; ze beïnvloedt de berekening niet.
Dit zijn illustratieve grenzen die voor dit programma zijn gekozen. Bij de maximaal toegestane invoer is het resultaat 333333833333500000, wat in UInt64 past. Validatie gebeurt vóór het indienen, zodat een ongeldig verzoek direct een fout oplevert en er geen bewerking in de wachtrij wordt geplaatst. De invoerbeperking zorgt er ook voor dat het bereik van de lus geldig blijft.
Elk geaccepteerd verzoek levert één opgeslagen resultaat op wanneer de bewerking is voltooid. Resultaten worden aan een array toegevoegd, zodat herhaalde identificaties eerdere resultaten niet overschrijven. De demonstratie gebruikt oplopende, unieke identificaties en sorteert op identificatie voor overzichtelijke uitvoer; die weergavevolgorde doet geen uitspraak over de uitvoeringsvolgorde.
Dit contract gaat ervan uit dat de wachtrij beschikbaar blijft en dat de bewerkingen worden voltooid. Het voorbeeld biedt geen ondersteuning voor annulering. Als je later annulering toevoegt, is een aparte uitkomst nodig voor niet-voltooide verzoeken, omdat Apple annulering documenteert als een verzoek om te stoppen, waarbij lopend werk zelf moet reageren. Apple’s documentatie over OperationQueue beschrijft de levensduur en annulering van bewerkingen.
Bouw een volledig programma met een gedeelde limiet
Gebruik een Mac met een Swift 6.2-compiler of nieuwer en de macOS SDK die beschikbaar is via Xcode of de bijbehorende opdrachtregelprogramma’s. Sla het volgende volledige programma op als main.swift. De onderstaande opdrachten selecteren expliciet niet-geïsoleerde standaardisolatie, zodat de berekeningshelpers onafhankelijk blijven van de isolatie-instellingen van een appdoel. Swift documenteert deze compilerinstelling in SE-0466: Control default actor isolation inference.
Het resultatenlog gebruikt NSLock om zowel de array als de activiteitstellers te beschermen. De conformiteit met @unchecked Sendable legt de verantwoordelijkheid voor die synchronisatie bij de implementatie: elke toegang tot veranderlijke status gebruikt dezelfde lock. Apple documenteert deze verantwoordelijkheid bij Sendable en de bescherming van kritieke secties bij NSLock.
import Foundation
enum InputError: Error {
case invalidIterations
}
struct Calculation: Sendable {
let id: Int
let iterations: Int
}
func sumOfSquares(_ job: Calculation) -> UInt64 {
var sum: UInt64 = 0
for integer in 1...job.iterations {
let value = UInt64(integer)
sum += value * value
}
return sum
}
final class ExecutionLog: @unchecked Sendable {
private let lock = NSLock()
private var active = 0
private var peak = 0
private var rows: [(id: Int, value: UInt64)] = []
func begin() {
lock.lock()
defer { lock.unlock() }
active += 1
peak = max(peak, active)
}
func finish(id: Int, value: UInt64) {
lock.lock()
defer { lock.unlock() }
rows.append((id: id, value: value))
active -= 1
}
func snapshot() -> (
rows: [(id: Int, value: UInt64)], peak: Int
) {
lock.lock()
defer { lock.unlock() }
return (rows, peak)
}
}
final class CalculationPool {
private let queue = OperationQueue()
private let log = ExecutionLog()
init(limit: Int) {
precondition(limit > 0)
queue.name = "Synthetic calculations"
queue.maxConcurrentOperationCount = limit
}
func submit(id: Int, iterations: Int) throws {
guard (1...1_000_000).contains(iterations) else {
throw InputError.invalidIterations
}
let job = Calculation(id: id, iterations: iterations)
queue.addOperation { [log] in
log.begin()
let value = sumOfSquares(job)
log.finish(id: job.id, value: value)
}
}
// Call only after the producer has stopped submitting work.
// This blocking method is for the command-line demonstration.
func finishForDemo() -> (
rows: [(id: Int, value: UInt64)], peak: Int
) {
queue.waitUntilAllOperationsAreFinished()
return log.snapshot()
}
}
@main
struct Demo {
static func main() throws {
let limit = 2
let pool = CalculationPool(limit: limit)
try pool.submit(id: 1, iterations: 100_000)
try pool.submit(id: 2, iterations: 200_000)
try pool.submit(id: 3, iterations: 300_000)
let report = pool.finishForDemo()
let rows = report.rows.sorted { $0.id < $1.id }
precondition(rows.map { $0.id } == [1, 2, 3])
precondition(rows.map { $0.value } == [
UInt64(333338333350000),
UInt64(2666686666700000),
UInt64(9000045000050000)
])
precondition(report.peak >= 1 && report.peak <= limit)
for row in rows {
print("Job \(row.id): \(row.value)")
}
print("Observed peak: \(report.peak)")
}
}
Compileer en voer het uit vanuit de map met main.swift:
swiftc -swift-version 6 -default-isolation nonisolated \
-parse-as-library main.swift -o calculation-demo
./calculation-demo
De drie resultaatregels zouden moeten zijn:
Job 1: 333338333350000
Job 2: 2666686666700000
Job 3: 9000045000050000
De laatste regel rapporteert een waargenomen piek van 1 of 2. Een limiet staat overlap toe, maar vereist die niet. De verwachte sommen zijn onafhankelijk gecontroleerd met gehele getallen en de formule n * (n + 1) * (2 * n + 1) / 6. Het Swift-programma is nagekeken, maar kon niet worden gecompileerd in de onderzoeksomgeving omdat er geen Swift-uitvoerbaar bestand was geïnstalleerd.
Waarom geldt deze limiet voor de daadwerkelijke berekening?
Elke bewerking start de activiteitsteller, voert de volledige berekening synchroon uit, registreert het resultaat en keert daarna terug. Er wordt geen aanvullende asynchrone berekening gestart die buiten die closure doorgaat. Door het aantal uitgevoerde bewerkingen te beperken, beperk je dus ook deze berekeningen.
Behoud die relatie wanneer je het programma aanpast. Als een bewerking alleen een aparte Task aanmaakt en meteen terugkeert, ziet de wachtrij de bewerking als voltooid terwijl die taak mogelijk nog aan het rekenen is. De limiet voor bewerkingen geldt dan voor de korte closures die de taken starten. Dit volgt uit de gedocumenteerde levensduur van bewerkingen en de API voor blokindiening. Apple’s overzicht van OperationQueue en addOperation(_:) beschrijven deze onderdelen.
De lock van het log beschermt alleen korte gedeelten met administratieve handelingen. Als de lock tijdens sumOfSquares(_:) wordt vastgehouden, wordt de toegang tot de berekening geserialiseerd en kan de beoogde overlap niet plaatsvinden. Elke keer dat de lock wordt vergrendeld en ontgrendeld, gebeurt dat binnen één synchrone methode. Dat voldoet aan Apple’s vereiste dat een NSLock wordt ontgrendeld op de thread die hem heeft vergrendeld. Apple’s documentatie over NSLock licht dit vereiste toe.
Voer dezelfde drie verzoeken uit met limit ingesteld op 1 om een aanpassing te controleren, en herstel daarna 2. Beide uitvoeringen moeten alle drie de exacte resultaten behouden. De seriële uitvoering hoort een piek van 1 te rapporteren; de tweede uitvoering moet binnen 2 blijven. Deze controles testen het behoud van resultaten en de waargenomen limiet voor berekeningen zonder uit te gaan van een bepaalde planningsvolgorde.
Koppel binnenkomende desktopacties zonder de interface te blokkeren
Behoud één CalculationPool in het onderdeel dat verantwoordelijk is voor de desktopberekeningsfunctie. Voor elke binnenkomende actie roep je submit(id:iterations:) één keer aan. Maak de pool niet opnieuw aan binnen de actiehandler, omdat elke nieuwe instantie een eigen wachtrij en limiet heeft.
De demonstratie roept finishForDemo() pas na de laatste indiening aan, zodat het opdrachtregelproces resultaten kan verzamelen voordat het afsluit. Deze methode blokkeert de aanroepende thread. Gebruik in een desktopapp resultaatdoorgifte per resultaat in plaats van deze methode aan te roepen vanuit een knophandler of de main actor. Apple documenteert expliciet het blokkerende gedrag van waitUntilAllOperationsAreFinished().
Breid voor integratie met de interface de indienings-API uit met een resultaatcallback die geïsoleerd is tot MainActor en als @Sendable is gemarkeerd. Plan na het berekenen van een resultaat binnen de bewerking een Task in met een expliciete @MainActor-closure die deze callback aanroept. Leg de onveranderlijke resultaatwaarde en de callback vast. Houd alleen de interface-update binnen die closure; laat de berekening in de bewerking. Swift documenteert zowel syntaxis voor main-actor-closures als vastgelegde waarden in sendable closures.
Voor die interfacevariant is een aanvullende definitie van voltooiing nodig: de berekening voltooien en het resultaat weergeven zijn afzonderlijke gebeurtenissen. Het opdrachtregelprogramma wacht op berekeningen en synchrone opslag van resultaten. Een afzonderlijk geplande interfacecallback kan daarna nog worden uitgevoerd.
Wat gebeurt er wanneer verzoeken sneller binnenkomen dan berekeningen worden uitgevoerd?
De instelling voor gelijktijdigheid begrenst het aantal uitgevoerde bewerkingen, terwijl aanvullend ingediend werk in de wachtrij kan blijven staan. Ook kan de array met voltooide resultaten in het voorbeeld blijven groeien. Dit voorbeeld biedt dus een limiet voor de gelijktijdigheid van berekeningen, maar geen limiet voor het totale geheugengebruik of de achterstand. Apple documenteert dat de wachtrij bewerkingen bewaart totdat ze zijn voltooid. Het overzicht van OperationQueue licht deze opslag toe.
Als de desktopfunctie aanhoudende pieken kan genereren, kies dan een beleid voor toelating voordat je het uitbreidt: accepteer slechts een vast aantal openstaande verzoeken en meld afwijzingen, pauzeer verdere indieningen of vervang een wachtende synthetische preview door een nieuwere aanvraag wanneer dat gedrag uitdrukkelijk de bedoeling is. Beheer aantallen voor toelating en updates bij voltooiing vanuit één gesynchroniseerd onderdeel. De limiet voor lopend werk alleen kan deze beleidsregels niet implementeren.
Voor afzonderlijke synchrone verzoeken is het basisontwerp klein: behoud één wachtrij, stel de limiet eenmaal in, houd elke berekening binnen haar bewerking en synchroniseer gedeelde resultaten. Behoud deze grenzen wanneer je desktopacties koppelt en voeg pas een expliciet beleid voor wachtrijachterstand toe wanneer het aankomstpatroon daarom vraagt.
Bronnen
- OperationQueue — Apple.
- maxConcurrentOperationCount — Apple.
- addOperation(_:) — Apple.
- waitUntilAllOperationsAreFinished() — Apple.
- NSLock — Apple.
- Sendable — Apple.
- Concurrency — The Swift Programming Language — Swift.org.
- TaskGroup.swift — Swift-standaardbibliotheek: broncode en documentatie — Swift.org.
- SE-0466: Control default actor isolation inference — Swift.org.