Per un’app Mac che deve completare calcoli asincroni locali nell’ordine di invio, usa un metodo sincrono di invio isolato al main actor che concateni ogni attività alla precedente. L’isolamento dell’attore, da solo, non garantisce quest’ordine.

Questa guida realizza una piccola coda per calcoli interi sintetici. Definisce esattamente quando avviene l’invio, preserva l’ordine attraverso i punti di sospensione, restituisce il risultato di ogni calcolo e consente ai lavori successivi di proseguire anche se uno precedente fallisce.

Che cosa significa rigoroso ordine di invio?

Supponiamo che una calcolatrice desktop invii i lavori A, B e C. Il contratto richiesto è: il calcolo di A termina prima che inizi quello di B, e B termina prima che inizi C. Attendere tre attività avviate indipendentemente in ordine alfabetico ordinerebbe soltanto l’osservazione dei risultati.

Il confine dell’invio è importante. In questa implementazione, l’invio avviene quando viene eseguito il metodo submit, isolato al main actor, e viene installato il collegamento successivo nella catena. Chiamate sincrone consecutive dallo stesso contesto del main actor stabiliscono l’ordine A, B, C senza sospendersi tra loro.

Se attività indipendenti richiedono l’invio in concorrenza, il loro ordine di arrivo non è specificato. La coda conserva l’ordine in cui il suo metodo di invio le accetta. Se l’applicazione richiede un ordine preciso tra quei produttori, stabiliscilo prima dell’invio, ad esempio raccogliendo un lotto finito in un array ordinato esplicitamente.

Il progetto usa il main actor per il breve passaggio di gestione. Apple definisce MainActor come un attore singleton il cui executor è equivalente alla coda di dispatch principale. Il calcolo vero e proprio qui sotto è affidato a un attore worker separato. Documentazione di MainActor

Perché un attore o una coda seriale di operazioni non bastano?

Un attore esegue il lavoro isolato all’attore un pezzo alla volta, ma un metodo asincrono può sospendersi e consentire a un’altra chiamata di procedere. Il progetto degli attori di Swift afferma inoltre esplicitamente che non è garantito che le attività in attesa di un attore vengano eseguite nell’ordine in cui lo hanno atteso. Di conseguenza, inserire un intero calcolo asincrono in un attore non stabilisce, da solo, che un calcolo termini prima dell’inizio del successivo. SE-0306: Attori

Anche creare istanze Task separate non stabilisce alcuna dipendenza tra loro. Le attività possono iniziare a essere eseguite dopo la creazione; conservarne i riferimenti consente di attenderle o annullarle, ma non definisce una sequenza di esecuzione. Documentazione di Task

Foundation offre un meccanismo di ordinamento diverso. OperationQueue pianifica in base alla disponibilità, alla priorità e alle dipendenze; Apple consiglia di usare le dipendenze quando è importante un ordine di esecuzione specifico. Impostare maxConcurrentOperationCount su 1 limita la concorrenza, ma il requisito di ordinamento richiede comunque dipendenze esplicite. Documentazione di OperationQueue

Scegli il meccanismo in base alle esigenze già presenti nell’applicazione:

Situazione Approccio adatto Che cosa stabilisce l’ordine?
Un lotto noto in un’unica funzione asincrona Un ciclo che attende ogni calcolo prima di proseguire Flusso di controllo sequenziale
I lavori arrivano nel tempo da azioni desktop Un metodo sincrono di invio con una catena di attività Ogni calcolo attende il gate di completamento del predecessore
Operazioni Foundation già esistenti Dipendenze configurate prima dell’inserimento in coda Ogni operazione dipende dallo stato di completamento dell’operazione precedente

La catena di attività è utile in questo caso perché l’invio restituisce subito un handle mentre i calcoli vengono eseguiti in seguito. Per un lotto fisso senza nuovi lavori in arrivo, di solito il ciclo sequenziale richiede meno codice.

Crea una coda che attende il completamento dei calcoli asincroni

La coda memorizza un’attività di completamento privata e non generante errori, chiamata tail. Ogni invio acquisisce la coda corrente, crea un’attività di calcolo che la attende e sostituisce la coda con una nuova attività che osserva il completamento del calcolo.

L’attività di completamento attende result, che rappresenta il successo o il fallimento dopo il completamento di un’attività. Così un calcolo non riuscito può sbloccare il lavoro successivo senza perdere l’errore associato al suo handle. I chiamanti possono comunque recuperare quell’errore tramite l’handle. Documentazione del risultato di Task

Quello che segue è un esempio completo da riga di comando per un Mac con toolchain Swift 6. Salvalo come OrderedDemo.swift. Il sorgente è stato esaminato, ma non è stato possibile compilarlo in questo ambiente di ricerca perché non era disponibile un compilatore Swift.

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)")
    }
}

Compila ed esegui il programma salvato dalla sua cartella nel Terminale:

swiftc -swift-version 6 -parse-as-library OrderedDemo.swift -o OrderedDemo
./OrderedDemo

L’output previsto deriva dagli input dell’esempio e dalla catena di dipendenze:

start A
finish A
start B
finish B
start C
finish C
Answers: [9, 16, 25]

Le asserzioni controllano sia i valori restituiti sia i confini dei calcoli. Stampare semplicemente i risultati attendendo gli handle in ordine di invio non dimostrerebbe che i calcoli siano stati eseguiti in quell’ordine.

Il worker chiama intenzionalmente Task.yield() affinché il calcolo includa una possibilità di sospensione. Il rilascio consente l’esecuzione di altre attività, anche se l’executor potrebbe riprendere subito la stessa attività. L’ordinamento della coda deriva quindi dalle sue dipendenze, non dall’ipotesi che il rilascio faccia passare l’esecuzione a un’altra attività. Documentazione di Task yield

Perché la catena mantiene l’ordine attraverso le sospensioni?

Tre dettagli garantiscono questo comportamento. Primo, submit non contiene punti di sospensione, quindi un altro invio al main actor non può inserirsi mentre acquisisce e sostituisce tail. Swift documenta che il codice isolato a un attore, senza punti di sospensione, viene eseguito senza che un’altra attività dell’attore interrompa quella sequenza. Concorrenza: attori

Secondo, ogni lavoro attende il predecessore acquisito prima di invocare la closure di calcolo. Accedere a value di un’attività non generante errori attende il completamento di quell’attività. Il lavoro successivo può eseguire in anticipo la logica di attesa, ma non può iniziare prima il proprio calcolo. Documentazione della proprietà value di Task non generante errori

Terzo, il gate del predecessore si completa solo dopo che il lavoro precedente ha prodotto il proprio risultato o errore. Seguendo a ritroso i collegamenti si stabilisce la sequenza per ogni invio accettato: C attende il gate di completamento di B, che attende B, che attende il gate di completamento di A.

Questa garanzia di ordinamento riguarda il lavoro atteso all’interno di ogni closure inviata. Se una closure avvia un’altra attività non strutturata e termina senza attenderla, quel lavoro aggiuntivo può proseguire oltre la closure. Prima di restituire, includi nell’operazione attesa tutti i passaggi di calcolo richiesti.

Per l’integrazione desktop, mantieni un’unica istanza della coda nel controller o nel modello del main actor che gestisce le azioni interessate. Chiama direttamente submit da quel contesto, poi usa l’handle restituito per osservare il completamento. Creare prima un’attività indipendente e inviare il lavoro solo dopo il suo avvio sposterebbe il confine dell’invio al momento della sua successiva esecuzione.

Come devono comportarsi gli errori e l’annullamento?

Questa implementazione adotta una politica che prosegue dopo un errore. Un lavoro fallito conserva il proprio errore, il suo gate di completamento termina e il successivo lavoro accettato può iniziare il calcolo. L’esempio esamina result di ogni handle invece di abbandonare i risultati rimanenti dopo il primo errore.

Per verificare in modo semplice il fallimento, cambia l’input di B da 4 a 1001. Il worker lo rifiuta prima della moltiplicazione. Aggiorna le risposte attese a [9, 25] e la traccia attesa a start A, finish A, start B, start C, finish C. Il programma dovrebbe stampare l’errore e calcolare comunque C. Il limite dell’input è un contratto a scopo illustrativo, scelto per mantenere semplice questo calcolo.

L’annullamento è cooperativo. La coda controlla l’annullamento dopo aver atteso il predecessore e il worker lo ricontrolla dopo il rilascio. Task.checkCancellation() genera CancellationError se è stato richiesto l’annullamento. Documentazione di Task checkCancellation

Per controllare l’annullamento prima dell’esecuzione, chiama b.cancel() subito dopo aver inviato C e prima del ciclo dei risultati. Con questi invii consecutivi al main actor, B non può ancora aver iniziato il proprio calcolo. Modifica le risposte attese in [9, 25] e rimuovi dalla traccia attesa entrambe le voci di B. B attende comunque il gate di completamento di A prima di segnalare l’annullamento; poi C prosegue.

Mantieni privati i gate di completamento. Rappresentano la gestione della catena, mentre gli handle restituiti rappresentano i singoli calcoli. Annullare un lavoro non significa che il calcolo precedente debba interrompersi o che i calcoli successivi debbano iniziare mentre è ancora in esecuzione.

Se l’applicazione deve invece interrompere l’intero lotto dopo un errore, serve una politica esplicita per il lotto. Modificare il gate affinché propaghi gli errori cambierebbe quali lavori successivi vengono eseguiti e come vanno riportati i loro esiti.

Quando conviene usare le dipendenze di Foundation?

Se l’applicazione desktop rappresenta già il lavoro con oggetti Foundation Operation, stabilisci una dipendenza tra ogni operazione e quella precedente prima di inserirle in coda. Foundation considera soddisfatta la dipendenza quando l’operazione precedente termina, sia che abbia avuto successo sia che sia fallita; il comportamento che dipende dal successo richiede una gestione separata dell’esito. Documentazione di Operation

Fai attenzione quando integri funzioni asincrone Swift in questo modello. Un’operazione sincrona che avvia un Task e restituisce subito non rappresenta l’intera durata dell’attività. Un’operazione asincrona personalizzata deve gestire correttamente il proprio stato di esecuzione e di completamento, comprese le notifiche necessarie, affinché le operazioni dipendenti attendano il lavoro previsto. Apple descrive queste responsabilità nella documentazione sulle operazioni asincrone e sulla segnalazione dello stato di completamento. Documentazione del ciclo di vita di Operation

Per la calcolatrice asincrona locale sviluppata qui, la catena di attività rende esplicito il contratto: accettare gli invii in modo sincrono, attendere il completamento del predecessore, eseguire il calcolo completo ed esporne l’esito. Mantieni questi confini quando colleghi la coda alle azioni desktop e verifica l’esecuzione sia con tracce di avvio e completamento sia con i valori restituiti.

Fonti