Verwende eine Swift-Task-Group, um konkurrierende Berechnungen in der Reihenfolge ihres Abschlusses zu erfassen, nil zu überspringen und die übrigen Tasks abzubrechen, sobald ein Wert eintrifft. Dieses Mac-Kommandozeilenbeispiel findet eine brauchbare Rechteckanordnung für synthetische Kacheln und erklärt, wann das Ergebnis tatsächlich zurückgegeben werden kann.

Festlegen, was als erstes brauchbares Ergebnis gilt

Angenommen, ein kleines Desktop-Layout-Tool muss 221 synthetische Kacheln rechteckig anordnen. Mehrere lokale Berechnungen durchsuchen verschiedene mögliche Zeilenzahlen. Jeder nichttriviale Faktor ist akzeptabel: 13 Zeilen und 17 Spalten funktionieren ebenso wie 17 Zeilen und 13 Spalten. Die Aufgabe des Aufrufers besteht darin, die gültige Berechnung zu übernehmen, die zuerst abgeschlossen wird.

Das ist eine Vereinbarung zur Abschlussreihenfolge. Sie wählt weder den kleinsten Faktor noch das ausgewogenste Rechteck oder die zuerst aufgeführte Berechnung aus. Apple dokumentiert, dass aufeinanderfolgende Ergebnisse einer Task-Group in der Reihenfolge ihres Abschlusses eintreffen, unabhängig von der Reihenfolge, in der sie hinzugefügt wurden. TaskGroup.next(isolation:)

Lege das Verhalten fest, bevor du den Helper schreibst:

Situation Erforderliches Verhalten
Eine Berechnung wird mit nil abgeschlossen Weiter auf andere Berechnungen warten.
Eine Berechnung wird mit einem Wert abgeschlossen Diesen auswählen und den Abbruch der übrigen Arbeit anfordern.
Alle Berechnungen geben nil zurück nil zurückgeben.
Die Berechnungsliste ist leer nil zurückgeben.
Ein Abbruch des übergeordneten Tasks wird erkannt Die Auswahl von Ergebnissen beenden und nil zurückgeben.

Die letzte Zeile ist eine bewusste API-Entscheidung für dieses Beispiel. Hier bedeutet nil, dass der Aufruf keinen brauchbaren Wert liefert; zwischen einer erfolglosen Suche und einem Abbruch wird nicht unterschieden. Wenn dein Aufrufer diese Unterscheidung benötigt, verwende einen expliziten Ergebnistyp oder einen Vertrag mit Fehlerauslösung.

Ein vollständiges lokales Beispiel auf deinem Mac erstellen

Verwende eine Swift-6-Toolchain oder neuer. Prüfe sie im Terminal mit swift --version. Falls Swift nicht verfügbar ist, folge den offiziellen Anweisungen unter Install Swift — macOS. Das Programm verwendet Foundation und Swifts Nebenläufigkeits-APIs ohne externe Paketabhängigkeiten.

Erstelle mit diesen Befehlen ein neues ausführbares Paket. Dabei wird der von Swift.org dokumentierte Ablauf zum Erstellen und Ausführen von Paketen verwendet. Build a Command-line Tool

mkdir FirstNonNilDemo
cd FirstNonNilDemo
swift package init --name FirstNonNilDemo --type executable

Ersetze den generierten ausführbaren Quellcode durch das vollständige Programm unten. Je nach generierter Struktur kann die Datei Sources/FirstNonNilDemo/FirstNonNilDemo.swift oder Sources/main.swift heißen. Verwende eine einzige Einstiegspunktdatei mit dem Namen FirstNonNilDemo.swift; falls die Vorlage main.swift erstellt hat, benenne diese Datei um, bevor du das @main-Programm hinzufügst. Swifts Anleitung dokumentiert, dass ein Programm entweder main.swift oder einen @main-Einstiegspunkt verwendet. 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.")
            }
        }
    }
}

Führe das Paket in seinem Verzeichnis aus:

swift run FirstNonNilDemo

Für 221 sind sowohl 221 tiles: 13 rows x 17 columns als auch 221 tiles: 17 rows x 13 columns akzeptabel. Für 223 meldet das Programm, dass es in den angegebenen Bereichen keinen Faktor findet. Der Text tiles erzeugt die Eingabemeldung. Dies sind erwartete Ergebnisse, kein aufgezeichnetes Ausführungsprotokoll. Die Arithmetik wurde separat geprüft; da in der Rechercheumgebung kein Swift-Compiler verfügbar war, wurde das Swift-Programm geprüft, aber nicht ausgeführt.

Foundation übernimmt lediglich das Bereinigen umgebender Leerzeichen. Die Methode trimmingCharacters(in:) entfernt passende Zeichen an beiden Enden; Zeichen innerhalb der Zeichenfolge entfernt sie nicht. Daher wird 221 zu 221, während 2 21 für dieses Programm ungültig bleibt. trimmingCharacters(in:)

Warum der Helper nil überspringt, ohne spätere Ergebnisse zu verlieren

Der Ergebnistyp der Gruppen-Tasks ist Value?. Jeder Task liefert somit entweder einen brauchbaren Wert oder ein gewöhnliches leeres Ergebnis. Die for await-Schleife empfängt die abgeschlossenen Ergebnisse der einzelnen Tasks, und ihre innere optionale Bindung wählt nur einen nicht-nil-Kandidaten aus.

Bei direkter Verwendung von group.next() gibt es zwei verschiedene Arten von Leere. Der dokumentierte Rückgabetyp ist ChildTaskResult?. Bei optionalen Ergebnissen der einzelnen Tasks wird daraus Value??: Das äußere Optional zeigt an, ob ein weiteres Task-Ergebnis existiert, und das innere Optional zeigt an, ob dieser Task einen Wert gefunden hat. TaskGroup.next(isolation:)

Dieser Unterschied erklärt einen häufigen Fehler: ein einzelnes Ergebnis abzurufen und sofort zurückzugeben. Wenn die zuerst abgeschlossene Berechnung nil zurückgibt, können andere Tasks noch brauchbare Antworten haben. Die Schleife im vollständigen Beispiel verarbeitet weiter Ergebnisse, bis sie einen Wert findet oder die Gruppe ausgeschöpft ist.

Der Helper verwendet addTaskUnlessCancelled und prüft dessen booleschen Rückgabewert. Apple dokumentiert, dass diese Methode keinen Task hinzufügt, wenn die Gruppe bereits abgebrochen wurde. Wenn die Einreichungsschleife beendet wird, werden keine weiteren Tasks hinzugefügt, nachdem diese Bedingung erkannt wurde. addTaskUnlessCancelled(priority:operation:)

Die Berechnungen greifen auf die unveränderliche Kachelanzahl zu und geben ihre Antworten über die Gruppe zurück. Sie ändern keine gemeinsam genutzte Gewinner-Variable. Das generische Ergebnis muss Sendable entsprechen und erfüllt damit die dokumentierte Einschränkung für Task-Groups. withTaskGroup(of:returning:isolation:body:)

Warum die Auswahl eines Gewinners keine sofortige Rückgabe bedeutet

Die Rückgabe eines Werts aus dem Gruppen-Closure wählt die Antwort aus, aber der umgebende withTaskGroup wartet weiterhin, bis alle Tasks abgeschlossen sind. Apple dokumentiert ausdrücklich, dass selbst abgebrochene Tasks abgeschlossen sein müssen, bevor die Funktion zurückkehrt. withTaskGroup(of:returning:isolation:body:)

Der Block defer ruft group.cancelAll() auf, sobald das Closure beendet wird. Damit wird der Abbruch der übrigen Berechnungen angefordert. Ein Abbruch unterbricht nicht beliebige Berechnungen: Die Tasks müssen kooperieren und selbst frühzeitig beendet werden. TaskGroup

In diesem Beispiel prüft findFactor vor jedem möglichen Divisor Task.isCancelled. Jede Arbeitseinheit ist klein, sodass häufig die Möglichkeit besteht, sie zu beenden. Bei einer größeren Berechnung solltest du Prüfungen zwischen sinnvollen, begrenzten Arbeitsschritten einfügen. Eine Prüfung nur vor einem langen, unteilbaren Vorgang lässt diesen weiterlaufen, bis er abgeschlossen ist. Swifts Sprachhandbuch beschreibt den Abbruch als kooperativ und nennt die Rückgabe von nil als eine unterstützte Reaktion. Concurrency

Wenn eine konkurrierende Berechnung nie abgeschlossen wird und nicht auf einen Abbruch reagiert, kann dieser strukturierte Helper keine schnelle Rückgabe garantieren. Der unterstützte Weg ist, jeden Worker zeitlich zu begrenzen oder abbruchbewusst zu gestalten. Das Hinzufügen eines konkurrierenden Timeout-Ergebnisses hebt die Verpflichtung der Gruppe nicht auf, auf ihre Tasks zu warten.

Ein Abbruch des übergeordneten Tasks wird vor der Einreichung, beim Eintritt in den Task, innerhalb der Rechenschleife und vor der Annahme jedes Ergebnisses geprüft. Diese Prüfungen erkennen einen Abbruch jeweils zu bestimmten Zeitpunkten; sie schaffen keine atomare Regel, nach der ein Abbruch immer Vorrang vor einem Wert hat. Ein Abbruch, der nach der letzten Prüfung eintrifft, kann mit einer erfolgreichen Rückgabe zusammenfallen.

Die richtige Auswahlregel wählen und ihre Grenzen prüfen

Das synthetische Beispiel bietet einen hilfreichen Entscheidungstest: Sowohl 13 als auch 17 erfüllen den angegebenen Vertrag. Wenn dein Tool die kleinere Zeilenzahl benötigt, ist es falsch, ein beliebiges Ergebnis zu akzeptieren. Die Auswahl nach Abschlussreihenfolge ist nur dann sinnvoll, wenn alle akzeptierten Antworten für die Aufgabe des Aufrufers austauschbar sind.

Was der Aufrufer benötigt Geeigneter Ansatz
Einen beliebigen akzeptablen Wert, sobald Berechnungen abgeschlossen sind Die oben gezeigte Task-Group-Schleife verwenden.
Die zuerst erfolgreiche Berechnung in einer festgelegten Präferenzliste In Präferenzreihenfolge auswerten oder Indizes speichern und frühere Kandidaten vor der Auswahl auflösen.
Das kleinste oder bestbewertete Ergebnis Die relevanten Ergebnisse vor der Auswahl vergleichen.
Mehrere bekannte Ergebnisse, die gemeinsam benötigt werden async let in Betracht ziehen.

Swifts Sprachhandbuch zeigt async let für bekannte Child-Tasks, deren Ergebnisse später benötigt werden, und Task-Groups für die explizite Kontrolle über dynamisch hinzugefügte Tasks. Das Abrufen benannter Ergebnisse in einer gewählten Reihenfolge implementiert keine Auswahl in Abschlussreihenfolge. Concurrency

Verwende beim Anpassen des Programms diese Prüfungen:

  • Die Assertion für die leere Liste beibehalten: Sie sollte nil zurückgeben.
  • Die Assertion mit gemischten nil und 42 beibehalten: Sie sollte unabhängig von der Abschlussreihenfolge 42 zurückgeben.
  • Berechnungen testen, die alle nil zurückgeben.
  • Bei mehreren gültigen Antworten die Zugehörigkeit zur gültigen Menge prüfen, statt einen bestimmten Gewinner zu verlangen.
  • Jeden Worker auf begrenzten Fortschritt und Abbruchprüfungen untersuchen.
  • Eine erfolglose Suche als auf die angegebenen Bereiche beschränkt melden; nicht daraus schließen, dass es andernorts keine Antwort gibt.

Die drei winzigen Faktorsuchen veranschaulichen das Verhalten, aber keine Geschwindigkeitssteigerung. Für diese Beispielzahlen reicht eine sequenzielle Suche aus. Verwende das Muster mit konkurrierenden Berechnungen, wenn die konkrete Aufgabe davon profitiert, austauschbare Ergebnisse zu akzeptieren, und miss die Arbeitslast, bevor du entscheidest, wie viel Arbeit parallel ausgeführt werden soll.

Lege die Annahmeregel ausdrücklich fest, überspringe leere Ergebnisse einzelner Tasks und sorge dafür, dass die übrige Arbeit beendet werden kann. So erhält das Mac-Tool einen vorhersehbaren Ergebnisvertrag: der erste brauchbare Wert, den die Gruppe liefert, gefolgt vom Abschluss der kooperativen Beendigung ihrer Tasks.

Quellen