Verwenden Sie eine langlebige Foundation-OperationQueue und legen Sie vor dem Einreichen von Berechnungen deren maxConcurrentOperationCount fest. Für ein Mac-Dienstprogramm, das bei jedem Tastendruck eine Anfrage erhält, begrenzt dies die gleichzeitig ausgeführte Arbeit und lässt spätere Anfragen warten.
Dieses Tutorial erstellt die Berechnungs-Engine für diese Desktop-Aufgabe sowie ein eigenständiges Programm, mit dem sich ihr Verhalten prüfen lässt. Jede Anfrage enthält synthetische Ganzzahlen, und jedes Ergebnis bleibt im Speicher. Die Berechnung läuft synchron innerhalb ihrer Operation, sodass die Lebensdauer der Operation die tatsächliche Arbeit umfasst.
Welches Parallelitätswerkzeug eignet sich für einzeln eingehende Anfragen?
Eine OperationQueue nimmt einzelne Operationen entgegen, und ihre blockbasierte Methode addOperation(_:) verpackt jede eingereichte Closure in eine Operation. Behalten Sie die Queue in einem Objekt, das Anfragen überdauert, und reichen Sie für jedes eingehende Element eine Closure ein. Die Dokumentation von Apples addOperation(_:) beschreibt diese Einreichungsmethode.
Setzen Sie maxConcurrentOperationCount auf einen positiven Grenzwert wie 2. Apple definiert diese Eigenschaft als die maximale Anzahl von Operationen dieser Queue, die gleichzeitig ausgeführt werden können. Der Grenzwert gilt für genau diese Queue; für jede Anfrage eine eigene Queue zu erstellen, würde einen gemeinsamen Grenzwert vereiteln. Wird der Grenzwert später verringert, laufen bereits ausgeführte Operationen weiter. Apples Dokumentation zur Parallelitätsbegrenzung erläutert diese Grenzen.
Eine Swift-Aufgabengruppe hat eine andere Lebensdauer. Sie gehört in den Gültigkeitsbereich einer withTaskGroup und kann nicht außerhalb der Aufgabe verwendet werden, die sie erstellt hat. Außerdem wartet sie auf ihre untergeordneten Aufgaben, bevor sie zurückkehrt. Daher wird es nicht unterstützt, eine Aufgabengruppe als Eigenschaft für voneinander unabhängige zukünftige Tastendrücke zu speichern. Für diese synchrone Berechnungs-Engine bietet eine gespeicherte Operations-Queue die erforderliche Lebensdauer und eine explizite Parallelitätseinstellung. Die Dokumentation der Swift-Standardbibliothek zu Aufgabengruppen beschreibt diese Einschränkungen.
| Anordnung der Arbeit | Geeigneter Ansatz |
|---|---|
| Unabhängige synchrone Berechnungen gehen einzeln ein | Eine OperationQueue speichern und für jede Berechnung eine Operation einreichen. |
| Eine übergeordnete asynchrone Operation verwaltet eine endliche Sammlung untergeordneter Aufgaben | Eine Aufgabengruppe innerhalb des Gültigkeitsbereichs der übergeordneten Aufgabe verwenden; wenn ein numerischer Grenzwert erforderlich ist, die Zulassung ausdrücklich steuern. |
| Die Arbeit muss die Desktop-Oberfläche aktualisieren | Die Berechnung separat ausführen und das Ergebnis anschließend an Code übergeben, der auf MainActor isoliert ist. |
Die letzte Zeile betrifft die Ergebnisübergabe. Swifts Sprachleitfaden bezeichnet den Main Actor als den Ort für den Zustand der Benutzeroberfläche und zeigt ausdrücklich auf den Main Actor isolierte Closures. Halten Sie aufwendige Rechenarbeit aus diesem Übergabeschritt heraus. The Swift Programming Language: Concurrency erläutert die Main-Actor-Isolation.
Berechnung und Abschlussvertrag festlegen
Das Beispiel akzeptiert eine ganzzahlige Kennung und eine Anzahl von Iterationen von 1 bis 1_000_000. Bei einer Iterationszahl von n addiert es die Quadrate aller Ganzzahlen von 1 bis n. Es gibt die Kennung zusammen mit einem UInt64-Ergebnis zurück. Die Kennung dient zur Beschriftung der Ausgabe und beeinflusst die Berechnung nicht.
Diese Grenzen dienen als Beispiele für dieses Programm. Bei der maximal akzeptierten Eingabe beträgt das Ergebnis 333333833333500000 und passt damit in einen UInt64. Die Validierung erfolgt vor dem Einreichen, sodass eine ungültige Anfrage sofort einen Fehler auslöst und keine Operation in die Warteschlange einreiht. Die Eingabebeschränkung sorgt außerdem dafür, dass der Schleifenbereich gültig bleibt.
Jede akzeptierte Anfrage erzeugt beim Abschluss ihrer Operation ein gespeichertes Ergebnis. Die Ergebnisse sammeln sich in einem Array, sodass wiederholte Kennungen frühere Ergebnisse nicht überschreiben. Die Demonstration verwendet unterschiedliche, aufsteigende Kennungen und sortiert die Ausgabe nach Kennung, damit sie leichter zu lesen ist; diese Anzeigereihenfolge sagt nichts über die Ausführungsreihenfolge aus.
Dieser Vertrag setzt voraus, dass die Queue verfügbar bleibt und ihre Operationen abgeschlossen werden. Das Beispiel bietet keine Abbruchfunktion. Bei einer späteren Ergänzung von Abbrüchen muss ein eigener Ausgang für nicht abgeschlossene Anfragen festgelegt werden, denn Apple beschreibt den Abbruch als Aufforderung zum Beenden, auf die laufende Arbeit selbst reagieren muss. Apples Dokumentation zu OperationQueue erläutert die Lebensdauer und den Abbruch von Operationen.
Vollständiges Programm mit einer gemeinsamen Begrenzung erstellen
Verwenden Sie einen Mac mit einem Swift-Compiler ab Version 6.2 und dem macOS-SDK, das über Xcode oder dessen Befehlszeilenwerkzeuge verfügbar ist. Speichern Sie das folgende vollständige Programm als main.swift. Die folgenden Befehle wählen ausdrücklich eine nicht isolierte Standardisolation aus, sodass die Berechnungshelfer unabhängig von den Isolationseinstellungen eines App-Ziels bleiben. Swift dokumentiert diese Compilereinstellung in SE-0466: Control default actor isolation inference.
Das Ergebnisprotokoll verwendet NSLock, um sowohl sein Array als auch seine Aktivitätszähler zu schützen. Die Konformität zu @unchecked Sendable überträgt die Verantwortung für diese Synchronisierung auf die Implementierung: Jeder Zugriff auf veränderlichen Zustand verwendet dieselbe Sperre. Apple erläutert diese Verantwortung in Sendable und den Schutz kritischer Abschnitte in 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)")
}
}
Kompilieren Sie das Programm und führen Sie es im Verzeichnis aus, das main.swift enthält:
swiftc -swift-version 6 -default-isolation nonisolated \
-parse-as-library main.swift -o calculation-demo
./calculation-demo
Die drei Ergebniszeilen sollten lauten:
Auftrag 1: 333338333350000
Auftrag 2: 2666686666700000
Auftrag 3: 9000045000050000
Die letzte Zeile meldet eine beobachtete Höchstzahl von entweder 1 oder 2. Ein Grenzwert lässt Überlappungen zu, erzwingt sie aber nicht. Die erwarteten Summen wurden unabhängig mithilfe ganzzahliger Arithmetik und der Formel n * (n + 1) * (2 * n + 1) / 6 überprüft. Das Swift-Programm wurde geprüft, ließ sich in der Rechercheumgebung jedoch nicht kompilieren, da dort kein Swift-Executable installiert war.
Warum umfasst diese Begrenzung die tatsächliche Berechnung?
Jede Operation startet ihren Aktivitätszähler, führt die gesamte Berechnung synchron aus, speichert ihr Ergebnis und kehrt dann zurück. Aus dieser Closure wird keine weitere asynchrone Berechnung gestartet. Daher begrenzt die Begrenzung der ausgeführten Operationen auch die Anzahl dieser Berechnungen.
Behalten Sie diesen Zusammenhang bei, wenn Sie das Programm anpassen. Wenn eine Operation lediglich eine separate Task erstellt und sofort zurückkehrt, erkennt die Queue die Operation als abgeschlossen, während diese Aufgabe möglicherweise weiterrechnet. Die Operationsbegrenzung würde dann nur für die kurzen Start-Closures gelten. Dies folgt aus der dokumentierten Lebensdauer von Operationen und der blockbasierten Einreichungs-API. Apples Überblick zu OperationQueue und addOperation(_:) erläutern diese Zusammenhänge.
Die Sperre des Protokolls deckt nur kurze Abschnitte für Buchhaltung und Zähler ab. Würde sie während sumOfSquares(_:) gehalten, würde sie den Zugriff auf die Berechnung serialisieren und die gewünschte Überlappung verhindern. Jeder Sperrvorgang wird innerhalb einer synchronen Methode abgeschlossen, entsprechend Apples Vorgabe, dass ein NSLock auf dem Thread entsperrt werden muss, der ihn gesperrt hat. Apples Dokumentation zu NSLock erläutert diese Vorgabe.
Um eine Anpassung zu prüfen, führen Sie dieselben drei Anfragen mit limit auf 1 aus und setzen Sie es anschließend wieder auf 2. Beide Durchläufe müssen alle drei exakten Ergebnisse liefern. Der serielle Durchlauf sollte eine Höchstzahl von 1 melden; der zweite Durchlauf muss innerhalb von 2 bleiben. Diese Prüfungen kontrollieren, ob Ergebnisse erhalten bleiben und die beobachtete Berechnungsbegrenzung eingehalten wird, ohne eine bestimmte Reihenfolge der Planung vorauszusetzen.
Eingehende Desktop-Aktionen anbinden, ohne die Oberfläche zu blockieren
Speichern Sie ein einziges CalculationPool in der Komponente, die für die Desktop-Berechnungsfunktion zuständig ist. Jede eingehende Aktion ruft submit(id:iterations:) einmal auf. Erstellen Sie den Pool nicht bei jedem Aufruf der Aktionsbehandlung neu, denn jede neue Instanz hat eine eigene Queue und Begrenzung.
Die Demonstration ruft finishForDemo() erst nach der letzten Einreichung auf, damit der Befehlszeilenprozess die Ergebnisse vor dem Beenden einsammeln kann. Diese Methode blockiert den aufrufenden Thread. In einer Desktop-App sollten Sie Ergebnisse einzeln übergeben, statt diese Methode in einer Schaltflächenbehandlung oder auf dem Main Actor aufzurufen. Apple dokumentiert ausdrücklich das blockierende Verhalten von waitUntilAllOperationsAreFinished().
Für die Integration in die Benutzeroberfläche erweitern Sie die Einreichungs-API um einen an MainActor isolierten und mit @Sendable markierten Ergebnis-Callback. Planen Sie nach der Berechnung eines Ergebnisses innerhalb der Operation eine Task mit einer expliziten @MainActor-Closure, die diesen Callback aufruft. Erfassen Sie den unveränderlichen Ergebniswert und den Callback. Beschränken Sie diese Closure auf die Aktualisierung der Anzeige; die Berechnung bleibt in der Operation. Swift dokumentiert sowohl die Syntax für Main-Actor-Closures als auch Erfassungen durch Sendable-Closures.
Diese Variante für die Benutzeroberfläche benötigt eine zusätzliche Definition für den Abschluss: Das Ende der Berechnung und die Anzeige ihres Ergebnisses sind getrennte Ereignisse. Das Befehlszeilenprogramm wartet auf die Berechnungen und die synchrone Speicherung der Ergebnisse. Ein separat geplanter Callback für die Benutzeroberfläche kann später ausgeführt werden.
Was geschieht, wenn mehr Anfragen eingehen, als berechnet werden können?
Die Parallelitätseinstellung begrenzt die gleichzeitig ausgeführten Operationen, während zusätzliche eingereichte Arbeit in der Warteschlange verbleiben kann. Außerdem kann das Array mit den abgeschlossenen Ergebnissen im Beispiel weiter wachsen. Dieses Beispiel bietet somit eine Begrenzung der Berechnungsparallelität, aber keine Begrenzung des gesamten Speicherverbrauchs oder des Rückstaus. Apple dokumentiert, dass die Queue Operationen bis zu deren Abschluss behält. Der Überblick zu OperationQueue erläutert dieses Verhalten.
Wenn die Desktop-Funktion anhaltende Anfragewellen erzeugen kann, legen Sie vor einer Erweiterung eine Zulassungsregel fest: Nehmen Sie nur eine festgelegte Zahl noch offener Anfragen an und melden Sie Ablehnungen, pausieren Sie weitere Einreichungen oder ersetzen Sie eine ausstehende synthetische Vorschau durch eine neuere Anfrage, wenn dieses Verhalten ausdrücklich vorgesehen ist. Stimmen Sie Zulassungszähler und Abschlussaktualisierungen über einen gemeinsamen synchronisierten Besitzer ab. Die Begrenzung der laufenden Arbeit allein kann diese Regeln nicht umsetzen.
Für einzelne synchrone Anfragen ist das wesentliche Design überschaubar: eine Queue speichern, ihre Begrenzung einmal festlegen, jede Berechnung in ihrer Operation belassen und gemeinsame Ergebnisse synchronisieren. Behalten Sie diese Grenzen beim Anbinden von Desktop-Aktionen bei und fügen Sie erst dann eine ausdrückliche Regel für den Rückstau hinzu, wenn das Anfrageaufkommen dies erfordert.
Quellen
- OperationQueue — Apple.
- maxConcurrentOperationCount — Apple.
- addOperation(_:) — Apple.
- waitUntilAllOperationsAreFinished() — Apple.
- NSLock — Apple.
- Sendable — Apple.
- Concurrency — The Swift Programming Language — Swift.org.
- TaskGroup.swift — Swift-Standardbibliellquelltext und Dokumentation — Swift.org.
- SE-0466: Control default actor isolation inference — Swift.org.