Verwenden Sie für eine Mac-App, die lokale asynchrone Berechnungen in der Einreichungsreihenfolge abschließen muss, eine synchrone Einreichungsmethode auf dem Main Actor, die jede Aufgabe an ihre Vorgängerin anreiht. Die Actor-Isolation allein garantiert diese Reihenfolge nicht.
Diese Anleitung erstellt eine kleine Warteschlange für synthetische Ganzzahlberechnungen. Sie legt genau fest, wann die Einreichung erfolgt, erhält die Reihenfolge über Unterbrechungspunkte hinweg, gibt das Ergebnis jeder Berechnung zurück und lässt spätere Aufträge fortfahren, wenn ein früherer fehlschlägt.
Was bedeutet strikte Einreichungsreihenfolge?
Angenommen, ein Desktop-Taschenrechner reicht die Aufträge A, B und C ein. Der erforderliche Vertrag lautet: Die Berechnung von A ist abgeschlossen, bevor die Berechnung von B beginnt, und B ist abgeschlossen, bevor C beginnt. Drei unabhängig gestartete Aufgaben in alphabetischer Reihenfolge abzuwarten, würde lediglich die Beobachtung ihrer Ergebnisse ordnen.
Die Einreichungsgrenze ist entscheidend. In dieser Implementierung erfolgt die Einreichung, wenn die Main-Actor-isolierte Methode submit ausgeführt wird und das nächste Glied der Kette einsetzt. Aufeinanderfolgende synchrone Aufrufe aus einem Main-Actor-Kontext legen die Reihenfolge A, B, C fest, ohne dazwischen anzuhalten.
Wenn unabhängige Aufgaben gleichzeitig eine Einreichung anfordern, ist ihre Reihenfolge beim Eintreffen nicht festgelegt. Die Warteschlange bewahrt die Reihenfolge, in der ihre Einreichungsmethode die Aufträge annimmt. Wenn Ihre Anwendung eine bestimmte Reihenfolge unter diesen Produzenten benötigt, legen Sie diese Reihenfolge vor dem Einreichen fest, etwa indem Sie einen endlichen Stapel in einem explizit geordneten Array sammeln.
Für den kurzen Verwaltungsablauf wird der Main Actor verwendet. Apple definiert MainActor als Singleton-Actor, dessen Executor der Main Dispatch Queue entspricht. Die eigentliche Berechnung unten gehört zu einem separaten Worker-Actor. Dokumentation zu MainActor
Warum reichen ein Actor oder eine serielle Operationswarteschlange nicht aus?
Ein Actor führt Actor-isolierte Arbeit jeweils in einem Stück aus, aber eine asynchrone Methode kann angehalten werden und dadurch einem anderen Aufruf erlauben, fortzufahren. Swift stellt im Actor-Entwurf außerdem ausdrücklich fest, dass für Aufgaben, die auf einen Actor warten, nicht garantiert ist, dass sie in der Reihenfolge ausgeführt werden, in der sie ursprünglich darauf gewartet haben. Daher stellt das Einbetten einer vollständigen asynchronen Berechnung in einen Actor allein nicht sicher, dass jede Berechnung abgeschlossen ist, bevor die nächste beginnt. SE-0306: Actors
Das Erstellen separater Task-Instanzen schafft ebenfalls keine Abhängigkeit zwischen ihnen. Aufgaben können nach ihrer Erstellung mit der Ausführung beginnen. Wenn Sie ihre Handles behalten, können Sie auf sie warten oder sie abbrechen, aber dadurch entsteht keine Ausführungsreihenfolge. Task-Dokumentation
Foundation bietet einen anderen Mechanismus zur Reihenfolgensteuerung. OperationQueue plant Aufgaben nach Bereitschaft, Priorität und Abhängigkeiten ein. Apple empfiehlt Abhängigkeiten, wenn eine bestimmte Ausführungsreihenfolge wichtig ist. maxConcurrentOperationCount auf 1 zu setzen, begrenzt die Parallelität, doch die Reihenfolge erfordert weiterhin explizite Abhängigkeiten. Dokumentation zu OperationQueue
Wählen Sie den Mechanismus passend zu den Anforderungen Ihrer Anwendung:
| Situation | Geeigneter Ansatz | Was legt die Reihenfolge fest? |
|---|---|---|
| Ein bekannter Stapel in einer asynchronen Funktion | Eine Schleife, die jede Berechnung abwartet, bevor sie fortfährt | Sequenzieller Kontrollfluss |
| Aufträge treffen im Laufe der Zeit durch Desktop-Aktionen ein | Eine synchrone Einreichungsmethode mit einer Aufgabenkette | Jede Berechnung wartet auf das Abschluss-Gate ihrer Vorgängerin |
| Vorhandene Foundation-Operationen | Abhängigkeiten vor dem Einreihen konfigurieren | Jede Operation hängt vom Abschlusszustand der vorherigen ab |
Die Aufgabenkette eignet sich hier, weil die Einreichung sofort ein Handle zurückgibt, während die Berechnungen später laufen. Bei einem festen Stapel ohne später eintreffende Aufträge ist die sequenzielle Schleife meist mit weniger Code verbunden.
Erstellen Sie eine Warteschlange, die vollständige asynchrone Berechnungen abwartet
Die Warteschlange speichert eine private, nicht werfende Abschlussaufgabe namens tail. Bei jeder Einreichung wird der aktuelle Abschluss als Vorgänger erfasst, eine Berechnungsaufgabe erstellt, die darauf wartet, und der Abschluss durch eine neue Aufgabe ersetzt, die das Ende der Berechnung abwartet.
Die Abschlussaufgabe wartet auf result, das nach Abschluss einer Aufgabe entweder Erfolg oder Fehler darstellt. Dadurch kann eine fehlgeschlagene Berechnung den nächsten Auftrag freigeben, ohne den Fehler aus dem eigenen Handle der Berechnung zu verwerfen. Aufrufende können diesen Fehler weiterhin über das Handle abrufen. Dokumentation zu Task-Ergebnissen
Das folgende vollständige Befehlszeilenbeispiel ist für einen Mac mit einer Swift-6-Toolchain gedacht. Speichern Sie es als OrderedDemo.swift. Der Quelltext wurde geprüft, konnte in dieser Rechercheumgebung jedoch nicht kompiliert werden, da kein Swift-Compiler verfügbar war.
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)")
}
}
Kompilieren und starten Sie das gespeicherte Programm im Terminal aus seinem Verzeichnis:
swiftc -swift-version 6 -parse-as-library OrderedDemo.swift -o OrderedDemo
./OrderedDemo
Die erwartete Ausgabe ergibt sich aus den Eingaben und der Abhängigkeitskette des Beispiels:
start A
finish A
start B
finish B
start C
finish C
Answers: [9, 16, 25]
Die Assertions prüfen sowohl die zurückgegebenen Werte als auch die Berechnungsgrenzen. Ergebnisse lediglich auszugeben, während die Handles in Einreichungsreihenfolge abgewartet werden, würde nicht sicherstellen, dass die Berechnungen in dieser Reihenfolge ausgeführt wurden.
Der Worker ruft absichtlich Task.yield() auf, damit die Berechnung eine Gelegenheit zum Anhalten erhält. Durch das Freigeben der Ausführung können andere Aufgaben laufen, auch wenn der Executor dieselbe Aufgabe sofort fortsetzen kann. Die Reihenfolge der Warteschlange ergibt sich daher aus ihren Abhängigkeiten und nicht aus der Annahme, dass das Freigeben der Ausführung zu einem Aufgabenwechsel führt. Dokumentation zu Task-Yield
Warum bewahrt die Kette die Reihenfolge über Unterbrechungen hinweg?
Drei Details stellen diese Garantie her. Erstens enthält submit keinen Unterbrechungspunkt. Daher kann keine andere Einreichung auf dem Main Actor dazwischenkommen, während die Methode tail erfasst und ersetzt. Swift dokumentiert, dass Actor-isolierter Code ohne Unterbrechungspunkte ausgeführt wird, ohne dass eine andere Actor-Aufgabe diese Sequenz unterbrechen kann. Concurrency: Actors
Zweitens wartet jeder Auftrag auf seinen erfassten Vorgänger, bevor er die Berechnungs-Closure aufruft. Der Zugriff auf value einer nicht werfenden Aufgabe wartet, bis diese Aufgabe abgeschlossen ist. Der spätere Auftrag kann seine Warteverwaltung bereits früher ausführen, aber seine Berechnung nicht vorzeitig beginnen. Dokumentation zu value bei nicht werfenden Tasks
Drittens wird das Vorgänger-Gate erst abgeschlossen, nachdem der frühere Auftrag sein Ergebnis oder seinen Fehler geliefert hat. Verfolgt man die Verknüpfungen rückwärts, ergibt sich die Reihenfolge für jede angenommene Einreichung: C wartet auf Bs Abschluss-Gate, das auf B wartet, und B wartet auf As Abschluss-Gate.
Diese Reihenfolgen-Garantie gilt für die Arbeit, die innerhalb jeder eingereichten Closure abgewartet wird. Startet eine Closure eine weitere unstrukturierte Aufgabe und kehrt zurück, ohne auf sie zu warten, kann diese zusätzliche Arbeit die Closure überdauern. Führen Sie alle erforderlichen Berechnungsschritte in der abgewarteten Operation aus, bevor Sie zurückkehren.
Für die Desktop-Integration sollten Sie eine Warteschlangeninstanz im Main-Actor-Controller oder -Modell aufbewahren, das die betreffenden Aktionen verarbeitet. Rufen Sie submit direkt aus diesem Kontext auf und verwenden Sie dann das zurückgegebene Handle, um den Abschluss zu beobachten. Wenn Sie zuerst eine unabhängige Aufgabe erstellen und erst nach ihrem Start einreichen, verschiebt sich die Einreichungsgrenze auf den späteren Ausführungszeitpunkt dieser Aufgabe.
Wie sollten Fehler und Abbruch behandelt werden?
Diese Implementierung setzt auf Fortfahren nach einem Fehler. Ein fehlgeschlagener Auftrag behält seinen Fehler, sein Abschluss-Gate wird beendet und der nächste angenommene Auftrag kann mit der Berechnung beginnen. Das Beispiel prüft result jedes Handles, statt nach dem ersten Fehler die übrigen Ergebnisse aufzugeben.
Ändern Sie für eine einfache Fehlerprüfung Bs Eingabe von 4 in 1001. Der Worker weist sie vor der Multiplikation zurück. Aktualisieren Sie die erwarteten Antworten auf [9, 25] und die erwartete Ablaufverfolgung auf start A, finish A, start B, start C, finish C. Das Programm sollte den Fehler ausgeben und C trotzdem berechnen. Die Eingabegrenze ist ein beispielhafter Vertrag, der gewählt wurde, um die Arithmetik einfach zu halten.
Abbruch erfolgt kooperativ. Die Warteschlange prüft den Abbruch nach dem Warten auf ihren Vorgänger, und der Worker prüft ihn nach dem Freigeben der Ausführung erneut. Task.checkCancellation() wirft CancellationError, wenn ein Abbruch angefordert wurde. Dokumentation zu Task checkCancellation
Rufen Sie zum Prüfen des Abbruchs vor der Ausführung b.cancel() direkt nach dem Einreichen von C und vor der Ergebnisschleife auf. Bei diesen ununterbrochenen Main-Actor-Einreichungen kann B seine Berechnung noch nicht begonnen haben. Passen Sie die erwarteten Antworten auf [9, 25] an und entfernen Sie beide B-Einträge aus der erwarteten Ablaufverfolgung. B wartet weiterhin auf As Abschluss-Gate, bevor der Abbruch gemeldet wird; anschließend fährt C fort.
Halten Sie die Abschluss-Gates privat. Sie bilden die Verwaltungsstruktur der Kette ab, während die zurückgegebenen Handles die einzelnen Berechnungen darstellen. Den Abbruch eines Auftrags zu veranlassen bedeutet nicht, dass eine vorherige Berechnung stoppen oder eine spätere beginnen sollte, solange die vorherige noch läuft.
Wenn Ihre Anwendung stattdessen den gesamten Stapel nach einem Fehler anhalten soll, ist dafür eine explizite Stapelrichtlinie erforderlich. Werden Fehler über das Gate weitergegeben, ändert sich, welche späteren Aufträge ausgeführt werden und wie ihre Ergebnisse gemeldet werden müssen.
Wann sollten Sie stattdessen Foundation-Abhängigkeiten verwenden?
Wenn die Desktop-Anwendung Arbeit bereits als Foundation-Operation-Objekte abbildet, legen Sie vor dem Einreihen eine Abhängigkeit jeder Operation zu ihrer Vorgängerin fest. Foundation betrachtet die Abhängigkeit als erfüllt, sobald die Vorgängeroperation abgeschlossen ist, unabhängig davon, ob sie erfolgreich war oder fehlgeschlagen ist. Verhalten, das vom Erfolg abhängt, erfordert eine separate Auswertung des Ergebnisses. Dokumentation zu Operation
Gehen Sie beim Überführen asynchroner Swift-Funktionen in dieses Modell sorgfältig vor. Eine synchrone Operation, die eine Task startet und sofort zurückkehrt, bildet nicht die vollständige Lebensdauer der Aufgabe ab. Eine eigene asynchrone Operation muss ihren Ausführungs- und Abschlusszustand korrekt verwalten, einschließlich der erforderlichen Benachrichtigungen, damit abhängige Operationen auf die vorgesehene Arbeit warten. Apple beschreibt diese Zuständigkeiten unter asynchronen Operationen und der Meldung des Abschlusszustands. Dokumentation zum Lebenszyklus von Operationen
Bei dem hier entwickelten lokalen asynchronen Taschenrechner macht die Aufgabenkette den Vertrag sichtbar: Einreichungen synchron annehmen, auf den Abschluss der Vorgängeraufgabe warten, die vollständige Berechnung ausführen und ihr Ergebnis verfügbar machen. Bewahren Sie diese Grenzen bei der Anbindung der Warteschlange an Desktop-Aktionen und überprüfen Sie die Ausführung anhand von Start- und Abschlussprotokollen sowie der zurückgegebenen Werte.
Quellen
- MainActor — Apple
- SE-0306: Actors — Swift
- Task — Apple
- OperationQueue — Apple
- result — Apple
- yield() — Apple
- Concurrency — Swift
- value: Nicht werfende Aufgabe — Apple
- checkCancellation() — Apple
- Operation — Apple