Maak een Swift-berekening annuleerbaar door de taakreferentie te bewaren, cancel() aan te roepen wanneer daarom wordt gevraagd en in de lus op annulering te controleren. Dit macOS-voorbeeld houdt de interface ook responsief en voorkomt dat een geannuleerde uitvoering later alsnog een resultaat weergeeft.

Wat doet het annuleren van een Swift-taak eigenlijk?

Swift gebruikt coöperatieve annulering: een annuleringsverzoek markeert een taak, terwijl de actieve code bepaalt wanneer die stopt. Een CPU-lus die nooit op annulering controleert, kan doorgaan tot voltooiing. Task.checkCancellation() aanroepen werpt CancellationError op wanneer annulering wordt waargenomen. Apple-documentatie over annulering, checkCancellation().

De taak voor de lezer is hier concreet: voeg werkende knoppen Start en Cancel toe aan een kleine Mac-app die een synthetische som berekent. Alle invoer wordt lokaal gegenereerd. De berekening telt de vierkantswortels van opeenvolgende positieve gehele getallen op en levert pas één Double op wanneer alle gevraagde termen zijn verwerkt.

Kies het contract voor het resultaat voordat je annulering implementeert. Dit voorbeeld verwerpt een onvoltooide som en meldt annulering afzonderlijk. Een gedeeltelijke som retourneren vereist een ander contract dat aangeeft hoeveel termen zijn meegenomen; anders kan een aanroeper onvolledig werk aanzien voor het gevraagde antwoord. In de taalgids van Swift worden werpen, een leeg resultaat retourneren en gedeeltelijk werk retourneren beschreven als mogelijke reacties op annulering. Concurrency.

Er zijn twee verantwoordelijkheden om te implementeren: onnodige berekeningen stoppen en bepalen of een voltooide waarde nog in de interface thuishoort. Het voorbeeld regelt beide, ook als annulering wordt aangevraagd nadat de laatste controle van de worker is uitgevoerd maar voordat het resultaat wordt weergegeven.

Plaats annuleringscontroles in de berekening

Maak in Xcode een macOS-appproject met SwiftUI en Swift. Gebruik voor dit voorbeeld de taalmodus Swift 6 en een implementatiedoel van macOS 13 of hoger. Voeg het volgende volledige bestand toe, met de naam SyntheticCalculation.swift. Het is niet afhankelijk van de view.

// SyntheticCalculation.swift
import Swift

enum SyntheticCalculation {
    nonisolated static func sumOfSquareRoots(
        terms: Int,
        checkEvery: Int = 4_096
    ) throws -> Double {
        precondition(terms >= 0, "terms must be nonnegative")
        precondition(checkEvery > 0, "checkEvery must be positive")

        try Task.checkCancellation()

        var completed = 0
        var total = 0.0

        while completed < terms {
            try Task.checkCancellation()

            let count = min(checkEvery, terms - completed)
            let end = completed + count

            while completed < end {
                completed += 1
                total += Double(completed).squareRoot()
            }
        }

        try Task.checkCancellation()
        return total
    }
}

De functie controleert op annulering voordat ze begint, bij elke chunkgrens en voordat ze retourneert. Dit zijn bewuste stoppunten: een annuleringsverzoek tijdens een chunk wordt bij de volgende controle waargenomen. Task.checkCancellation() is synchroon, dus de berekening kan een gewone functie blijven die fouten werpt; er is geen await nodig om alleen annulering te controleren. checkCancellation().

De chunkberekening houdt ook de invoergrenzen helder. Nul termen levert nul op als de taak niet is geannuleerd. Negatieve termen en een niet-positieve chunkgrootte zijn programmeerfouten die met precondities worden afgedwongen. Door elke chunk op basis van het resterende aantal te berekenen, voorkom je dat het eindpunt voorbij terms komt.

De accumulator blijft lokaal in de worker. Een geworpen annuleringsfout beëindigt de functie zonder die accumulator te retourneren; zo wordt het gekozen contract voor een volledig resultaat nageleefd. Omdat dit optellingen met drijvende komma zijn, is de voltooide waarde een benadering van de wiskundige som.

Houd de knop Cancel responsief

Annuleringscontroles helpen niet als een knopactie geen kans krijgt om uit te voeren. Apple legt uit dat zware synchrone bewerkingen in een gewone Task die vanuit een SwiftUI-view is gemaakt, de hoofdactor nog steeds kunnen bezetten, omdat die taak de actorcontext van de omgeving overneemt. Apple documenteert een detached-taak als een ondersteunde manier om synchroon werk buiten die overgenomen context uit te voeren. De responsiviteit van apps verbeteren.

Hier vangt de detached worker alleen een onveranderlijk geheel getal op en roept hij een expliciet nonisolated synchrone functie aan. Alle interfacestatus blijft op de hoofdactor. Een afzonderlijke taak op de hoofdactor wacht op de waarde van de worker en werkt de weergave bij.

Vervang het gegenereerde app-entrybestand door het volgende volledige bestand en verwijder het gegenereerde bestand ContentView.swift als het nog bestaat. Zorg dat het doel precies één @main-declaratie bevat, zoals vereist door de levenscyclus van een SwiftUI-app. Samen vormen deze twee bestanden de voorbeeldapp. App.

// CancellationDemoApp.swift
import Foundation
import SwiftUI

@main
struct CancellationDemoApp: App {
    var body: some Scene {
        WindowGroup {
            CalculationView()
        }
    }
}

@MainActor
struct CalculationView: View {
    @State private var worker: Task<Double, Error>?
    @State private var runID: UUID?
    @State private var status = "Ready"

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Synthetic square-root sum")
                .font(.headline)
            Text(status)
                .textSelection(.enabled)

            HStack {
                Button("Start") {
                    start()
                }
                .disabled(worker != nil)

                Button("Cancel") {
                    guard let worker else { return }
                    worker.cancel()
                    status = "Cancellation requested"
                }
                .disabled(worker == nil)
            }
        }
        .padding(24)
        .frame(minWidth: 420, minHeight: 160)
        .onDisappear {
            worker?.cancel()
            runID = nil
            worker = nil
            status = "Ready"
        }
    }

    private func start() {
        guard worker == nil else { return }

        let id = UUID()
        let terms = 500_000_000
        runID = id
        status = "Calculating"

        let run = Task.detached(priority: .userInitiated) {
            try SyntheticCalculation.sumOfSquareRoots(terms: terms)
        }
        worker = run

        Task { @MainActor in
            do {
                let value = try await run.value
                guard runID == id else { return }

                if run.isCancelled {
                    status = "Cancelled"
                } else {
                    status = "Result: \(value)"
                }
            } catch is CancellationError {
                guard runID == id else { return }
                status = "Cancelled"
            } catch {
                guard runID == id else { return }
                status = "Failed: \(error)"
            }

            guard runID == id else { return }
            worker = nil
            runID = nil
        }
    }
}

worker bewaren is essentieel. Een taakreferentie laten vallen annuleert de taak niet; je kunt de taak dan niet meer via die referentie annuleren. Als je de value van de worker afwacht, neem je ook de fout waar. Detached taken vereisen expliciet beheer van hun levenscyclus, dus de knop en de handler voor het verdwijnen van de view annuleren de worker rechtstreeks. Alleen de taak annuleren die op het resultaat wacht, zorgt er niet voor dat de annulering wordt doorgegeven aan deze detached worker. Documentatie over detached taken.

Foundation’s UUID levert voor elke uitvoering een identificatie. De vergelijking van identificaties is een regel op applicatieniveau: zodra het verdwijnen van de view een uitvoering ongeldig maakt, kan de uiteindelijke voltooiing ervan de huidige status van deze view niet bijwerken. De identificatie stopt de berekening niet; worker.cancel() en de controles in de lus doen dat. UUID.

De knop Start blijft uitgeschakeld totdat de worker klaar is of een fout werpt. Als je op Cancel drukt, verschijnt eerst Cancellation requested; de voltooiingsobserver verandert dat in Cancelled wanneer die de uitkomst verwerkt. Zo blijft een annuleringsverzoek onderscheiden van de bevestiging dat de worker is beëindigd.

Hoe moet je een laat resultaat afhandelen?

Denk aan de volgende volgorde: de worker passeert zijn laatste controle, is klaar en daarna drukt de gebruiker op Cancel voordat de hoofdactor de waarde accepteert. Apple merkt expliciet op dat annulering na de laatste mogelijke controle geen waarneembaar effect op de worker hoeft te hebben. Een laatste controle verkleint dat tijdsinterval, maar kan het niet wegnemen. cancel().

Daarom controleert het voorbeeld run.isCancelled nadat een geslaagde waarde is afgewacht. Dit is een controle van de workerreferentie. Als je daar de statische Task.isCancelled gebruikt, controleer je de observertaak. De taalgids van Swift maakt onderscheid tussen het controleren van de huidige taak via de typeproperty en het controleren van een andere taak via de instantieproperty. Concurrency.

Dit stelt een concrete acceptatieregel vast: annulering die wordt verwerkt voordat het resultaat wordt geaccepteerd, krijgt voorrang. Als de acceptatie al is voltooid en de worker is gewist, is er voor de uitgeschakelde knop Cancel geen voltooid resultaat om in te trekken. Deze regel volgt de volgorde van acties op de hoofdactor, zonder te beloven dat annulering voltooid werk ongedaan maakt.

Kies een bruikbare interval voor annuleringscontroles

De standaardchunkgrootte van 4,096 termen is een illustratief startpunt. Kleinere chunks bieden vaker de gelegenheid om annulering waar te nemen en voeren meer controles uit. Grotere chunks verlagen de controlefrequentie, maar laten meer berekeningen tussen stoppunten plaatsvinden.

Stel voor een ruwe planningsberekening dat een chunk 4,096 termen bevat en dat elke term illustratief twee microseconden kost. Die chunk staat dan voor ongeveer 8.2 milliseconden rekenwerk. Dit zijn verzonnen invoerwaarden om de berekening te laten zien, geen metingen van Swift of een specifieke Mac. Taakplanning en interface-updates kunnen extra vertraging veroorzaken.

Waarneming Wat je moet inspecteren of aanpassen
De interface reageert niet meer tijdens de berekening Controleer of de lus in de detached worker draait en buiten de isolatie van de hoofdactor blijft.
De interface reageert wel, maar annulering duurt te lang Verklein checkEvery en meet de langste chunk.
Aan een chunk wordt later een dure bewerking toegevoegd Voeg binnen die bewerking stoppunten toe als de implementatie daarvan dat ondersteunt.
Een geannuleerde uitvoering geeft een resultaat weer Controleer de workerreferentie op het moment van acceptatie en behoud de controle op de uitvoeringsidentificatie.

Voeg voor deze lus niet alleen een annuleringshandler toe om een extra vlag in te stellen. De taak maakt de annuleringsstatus al beschikbaar en in de taalgids van Swift wordt uitgelegd dat stoppen coöperatief blijft, ook wanneer je een handler installeert. Voor deze lokale berekening volstaan expliciete controles als stopmechanisme. Concurrency.

Controleer voltooiing, annulering en herhaalde uitvoeringen

In de onderzoeksomgeving waren geen Swift-compiler en macOS-runtime beschikbaar, dus dit voorbeeld is geïnspecteerd maar niet gecompileerd of uitgevoerd. Bouw beide bestanden in Xcode en controleer vervolgens deze afzonderlijke gedragingen:

  1. Kleine voltooide uitkomst: wijzig terms tijdelijk in 4. Het weergegeven resultaat hoort ongeveer 6.146264369941973 te zijn, met enige afronding vanwege drijvende komma’s.
  2. Lege invoer: wijzig terms in 0. Een niet-geannuleerde uitvoering hoort nul weer te geven.
  3. Annulering tijdens het werk: zet 500_000_000 terug, druk op Start en daarna op Cancel terwijl de berekening loopt. Verwacht eerst Cancellation requested en vervolgens Cancelled, zonder gedeeltelijk resultaat.
  4. Nieuwe uitvoering: druk na voltooiing van de annulering opnieuw op Start. De nieuwe uitvoering hoort normaal te beginnen.
  5. Verdwijnen van de view: sluit een venster terwijl er een berekening loopt. De handler voor het verdwijnen hoort annulering aan te vragen en interface-updates van die uitvoering ongeldig te maken.

Het grote aantal termen is bedoeld om tijd te laten voor interactie, maar de duur hangt af van de Mac en de buildconfiguratie. Als de berekening klaar is voordat je kunt annuleren, verhoog je het aantal binnen het ondersteunde bereik voor gehele getallen. Controleer voltooide rekenresultaten met de kleine invoer in plaats van te wachten tot de grote demonstratie klaar is.

Het uiteindelijke ontwerp heeft een duidelijk stoppad en een heldere acceptatieregel: de knop annuleert de bewaarde worker, de chunkgrenzen laten de berekening een fout werpen en de hoofdactor accepteert alleen een niet-geannuleerd resultaat van de huidige uitvoering. Die drie keuzes maken annulering bruikbaar voor een gewone Mac-berekening.

Bronnen