Begrenzte iOS-Taskplanung auf dem Cloud-Mac prüfen

Begrenzte iOS-Taskplanung auf dem Cloud-Mac prüfen

Begrenzte iOS-Taskplanung auf dem Cloud-Mac prüfen

Wer zwanzig Anhänge im Stapel verarbeitet, begrenzt die Parallelität nicht dadurch, dass er alle Aufgaben in eine TaskGroup aufnimmt und anschließend die Ergebnisse einzeln abruft. Netzwerkverbindungen, Decodierpuffer und temporäre Dateien können trotzdem gleichzeitig zunehmen. Bei der Abnahme dieser Logik auf einem Cloud-Mac geht es nicht darum, möglichst schnell fertig zu werden, sondern um einen Nachweis: Bei einem Limit von drei muss die vierte Operation warten, bis ein Platz frei wird.

Dazu trennen wir den Scheduler von der Netzwerkimplementierung. Zuerst prüfen wir die Belegung der Plätze mit steuerbaren Operationen, danach binden wir den iOS-Anwendungscode an. So lassen sich bei einem Fehlschlag Planungsfehler von Schwankungen externer Dienste unterscheiden, ohne durch wiederholte Testläufe nach einer Erklärung suchen zu müssen.

Zuerst einen überprüfbaren Scheduling-Vertrag festlegen

Es gelten folgende Regeln: Die Reihenfolge der Eingaben bestimmt die Reihenfolge der Ausgaben, nicht aber die Reihenfolge der Fertigstellung. Eine leere Eingabe liefert ein leeres Array zurück. Das Parallelitätslimit muss größer als null sein. Wirft eine beliebige Kindaufgabe einen Fehler, schlägt der gesamte Aufruf fehl; Teilergebnisse werden nicht ausgeliefert. Nach dem Abbruch der Elternaufgabe sollen keine weiteren ausstehenden Arbeiten aktiv nachgereicht werden.

Begrenzt wird hier die Anzahl aktiver Aufrufe der übergebenen Operation – nicht die Zahl der Threads und auch nicht die Anfragerate. Startet eine Operation intern mehrere Anfragen, werden diese nicht einzeln vom äußeren Zähler erfasst. Ein Kontingent für Anfragen pro Sekunde muss separat umgesetzt werden; Parallelitätsplätze ersetzen keine Ratenbegrenzung.

Ein Abbruch ist ein kooperatives Signal, kein erzwungenes Beenden. Bevor die TaskGroup ihren Gültigkeitsbereich verlässt, wartet sie weiterhin auf das Ende ihrer Kindaufgaben. Ignoriert eine Operation den Abbruch, hält sie den gesamten Aufruf auf.

Nach jeder Fertigstellung die nächste Aufgabe nachreichen

Speichern Sie den folgenden Code als BoundedMap.swift. Führen Sie ihn auf einem Cloud-Mac mit installierter Swift-Toolchain aus und dokumentieren Sie zunächst mit swift --version die Version. Das Beispiel verwendet die für strikte Nebenläufigkeitsprüfung erforderlichen Sendable-Constraints und benötigt weder eine grafische Sitzung noch einen Simulator.

import Foundation

func boundedMap<Input: Sendable, Output: Sendable>(
    _ inputs: [Input],
    limit: Int,
    operation: @escaping @Sendable (Input) async throws -> Output
) async throws -> [Output] {
    precondition(limit > 0)

    return try await withThrowingTaskGroup(
        of: (Int, Output).self
    ) { group in
        var nextIndex = 0
        var completed: [(Int, Output)] = []
        completed.reserveCapacity(inputs.count)

        for index in 0..<min(limit, inputs.count) {
            try Task.checkCancellation()
            let input = inputs[index]
            group.addTask {
                try Task.checkCancellation()
                return (index, try await operation(input))
            }
            nextIndex += 1
        }

        while let result = try await group.next() {
            completed.append(result)
            try Task.checkCancellation()

            if nextIndex < inputs.count {
                let index = nextIndex
                let input = inputs[index]
                group.addTask {
                    try Task.checkCancellation()
                    return (index, try await operation(input))
                }
                nextIndex += 1
            }
        }

        try Task.checkCancellation()
        return completed.sorted { $0.0 < $1.0 }.map { $0.1 }
    }
}

Zu Beginn werden nur so viele Aufgaben eingereiht, wie das Limit zulässt. Erst wenn ein Ergebnis vorliegt, kommt eine neue Aufgabe hinzu. Eine lange Eingabeliste führt deshalb nicht dazu, dass ebenso viele wartende Aufgaben angelegt werden. Jedes Ergebnis enthält seinen Eingabeindex; am Ende wird damit die ursprüngliche Reihenfolge wiederhergestellt. „Früher fertig“ bedeutet also nicht versehentlich „weiter vorn im Ergebnis“.

Fehler werden durch group.next() weitergegeben. Verlässt die Closure ihren Gültigkeitsbereich mit einem Fehler, bricht die TaskGroup die verbleibenden Kindaufgaben ab und wartet auf deren Abschluss. Hier werden weder Fehler abgefangen, um anschließend weitere Aufgaben nachzureichen, noch unabhängige Hintergrundaufgaben gestartet, deren Lebensdauer von der Elternaufgabe entkoppelt ist.

Die tatsächliche Anzahl aktiver Operationen mit einer Actor-Sonde messen

Hängen Sie den folgenden Code an dieselbe Datei an und führen Sie anschließend swift BoundedMap.swift aus. Die Sonde erhöht den Zähler beim Eintritt in eine Operation und verringert ihn beim Verlassen. Während der Wartezeit kann der Actor weitere Aufrufe annehmen, sodass die Arbeit nicht versehentlich vollständig serialisiert wird.

actor Probe {
    private var active = 0
    private var peak = 0

    func run(_ value: Int) async throws -> Int {
        active += 1
        peak = max(peak, active)
        defer { active -= 1 }

        let delay = UInt64(5 - value % 5) * 10_000_000
        try await Task.sleep(nanoseconds: delay)
        return value * 2
    }

    func snapshot() -> (active: Int, peak: Int) {
        (active, peak)
    }
}

let probe = Probe()
let inputs = Array(0..<20)
let output = try await boundedMap(inputs, limit: 3) {
    try await probe.run($0)
}
let state = await probe.snapshot()
precondition(output == inputs.map { $0 * 2 })
precondition(state.active == 0)
precondition(state.peak <= 3)

Die unterschiedlichen Verzögerungen sollen die Abschlussreihenfolge durcheinanderbringen; sie sind kein Leistungsbenchmark. Prüfen Sie nicht, ob eine bestimmte Aufgabe nach einer bestimmten Millisekundenzahl fertig sein muss. Auch ein Spitzenwert unter drei ist kein Fehler, wenn etwa die Maschine stärker ausgelastet ist. Entscheidend ist: Der Spitzenwert überschreitet das Limit nicht, die Ergebnisse sind vollständig und nach dem Ende sind keine Operationen mehr aktiv.

Führen Sie zusätzlich getrennte Tests mit einem leeren Array, einem einzelnen Element und einem Limit von eins durch. Übernimmt der aufrufende Code das Limit aus einer Konfigurationsdatei, muss er null und negative Werte vorab zurückweisen. Die Vorbedingung im Beispiel eignet sich dazu, Programmierfehler offenzulegen, nicht zur direkten Verarbeitung von Benutzereingaben.

Fehler und Abbruch getrennt prüfen

Ein erfolgreicher Testfall beweist noch nicht, dass der Abbruch korrekt funktioniert. Erstellen Sie zunächst eine Operation, die für einen festgelegten Eingabewert zuverlässig einen Fehler wirft. Prüfen Sie, dass der Aufruf fehlschlägt und der Aktivzähler der Sonde am Ende wieder null ist. Legen Sie nicht fest, wie viele Aufgaben vor dem Fehler genau gestartet worden sein müssen: Bevor der Fehler die TaskGroup erreicht, können andere Aufgaben bereits fertig geworden sein und das Nachreichen weiterer Aufgaben ausgelöst haben.

Für den Abbruchtest sollte eine steuerbare Schranke verwendet werden: Die erste Gruppe von Operationen meldet ihren Eintritt. Sobald der Test diese Signale empfangen hat, bricht er die Elternaufgabe ab und öffnet anschließend die Schranke. Nach dem Passieren der Schranke prüfen die Operationen den Abbruchstatus. Der Test wartet auf das Ende der Elternaufgabe und stellt abschließend sicher, dass alle eingetretenen Operationen auch wieder beendet wurden.

Versuchen Sie nicht, anhand fester Wartezeiten zu erraten, ob Aufgaben bereits gestartet sind. Auch die Schranke selbst muss alle Wartenden freigeben können. Andernfalls könnte der Test einen Deadlock in der Testvorrichtung fälschlich als Hängen des Schedulers bewerten. Der Abnahmebericht sollte zwischen dem Zeitpunkt der Abbruchanforderung, dem Ende der Operationen und der Rückkehr der Elternaufgabe unterscheiden.

Bei der Integration die Grenzen der Plätze einhalten

Bei Netzwerkdownloads gehören der Download, die erforderliche Decodierung und das Aufräumen temporärer Dateien in denselben Operationsbereich. Wird der Platz bereits beim Empfang der Antwort freigegeben, während die Decodierung großer Bilder an eine Hintergrundaufgabe ausgelagert wird, ist zwar die Netzwerkparallelität begrenzt, der maximale Speicherbedarf aber weiterhin unkontrolliert.

Diese Implementierung hält sämtliche Ausgaben vor; ihr Speicherbedarf wächst mit der Gesamtmenge der Ergebnisse. Bei großen Dateien sollten daher Dateipfade und kleine Zusammenfassungen zurückgegeben werden, statt sämtliche Dateiinhalte in einem Array zu sammeln. Eine Pipeline, die Ergebnisse unmittelbar nach ihrer Fertigstellung verarbeitet, benötigt einen eigenen Vertrag für den Gegendruck auf der Ausgabeseite. Dieser Implementierung lässt sich nicht einfach ein konstanter Speicherbedarf zuschreiben.

Dokumentieren Sie abschließend die Toolchain-Version, die Testeingaben, das Limit, den Spitzenwert und die Anzahl beendeter Operationen. Nehmen Sie die Scheduler-Tests in ein reguläres Unit-Test-Target auf und ergänzen Sie für die echten Operationen Prüfungen des Abbruchs und der Ressourcenfreigabe. Sobald sich die drei Grenzen – Platzbelegung, Reihenfolge und Abschluss – zuverlässig nachweisen lassen, steht für den Wechsel auf einen anderen Cloud-Mac eine überprüfbare Verhaltensreferenz bereit.

Häufig gestellte Fragen

Begrenzt ein verzögerter Start jedes Tasks die Parallelität?

Nein. Versetzte Starts garantieren keine Obergrenze aktiver Tasks. Zunächst nur die erlaubte Anzahl starten und nach jedem Abschluss einen Task nachlegen.

Kehrt eine abgebrochene Taskgruppe sofort zurück?

Nicht zwingend. Eine strukturierte Taskgruppe wartet auf ihre Kind-Tasks. Diese müssen den Abbruch beachten und ihre zugrunde liegende Arbeit beenden.

Einen exklusiven Mac für deinen Build mieten?

Wähle die passende Mietdauer. Du erhältst einen kompletten physischen Mac exklusiv für dich – mit vollem Zugriff auf die macOS-Oberfläche und das Terminal. Modell, Standort und Verfügbarkeit zeigt die Konsole in Echtzeit an.

Nutze diese Schritte beim nächsten Archiv-Build.

Prüfe zuerst Mietdauer und Konfiguration, dann starte den Build.