Usa un gruppo di attività Swift per raccogliere calcoli concorrenti nell’ordine di completamento, ignorare nil e annullare le attività figlie rimanenti quando arriva un valore. Questo esempio da riga di comando per Mac trova una disposizione rettangolare utilizzabile per tessere sintetiche e spiega quando il risultato può effettivamente essere restituito.

Definire cosa conta come primo risultato utile

Supponiamo che un piccolo strumento desktop per la disposizione debba sistemare 221 tessere sintetiche in un rettangolo. Diversi calcoli locali cercano un numero diverso di righe. È accettabile qualsiasi fattore non banale: 13 righe e 17 colonne funzionano, così come 17 righe e 13 colonne. Il compito del lettore è accettare il primo calcolo valido che termina.

Questo è un contratto basato sull’ordine di completamento. Non seleziona il fattore più piccolo, il rettangolo più equilibrato o il calcolo elencato per primo. Apple documenta che i risultati successivi di un gruppo di attività arrivano nell’ordine di completamento, indipendentemente dall’ordine di inserimento. TaskGroup.next(isolation:)

Definisci il comportamento prima di scrivere l’helper:

Situazione Comportamento richiesto
Un calcolo termina con nil Continua ad attendere gli altri calcoli.
Un calcolo termina con un valore Selezionalo e richiedi l’annullamento del lavoro rimanente.
Tutti i calcoli restituiscono nil Restituisci nil.
L’elenco dei calcoli è vuoto Restituisci nil.
Viene rilevato l’annullamento dell’attività genitore Interrompi la selezione dei risultati e restituisci nil.

L’ultima riga è una scelta deliberata dell’API per questo esempio. Qui, nil significa che la chiamata non fornisce alcun valore utilizzabile; non distingue una ricerca non riuscita dall’annullamento. Se il chiamante ha bisogno di distinguere questi casi, usa un tipo di esito esplicito o un contratto che possa generare errori.

Creare un esempio locale completo sul Mac

Usa una toolchain Swift 6 o successiva. In Terminale, controllala con swift --version. Se Swift non è disponibile, segui le istruzioni ufficiali Installare Swift — macOS. Il programma usa Foundation e le API di concorrenza di Swift senza dipendenze da pacchetti esterni.

Crea un nuovo pacchetto eseguibile con questi comandi. La procedura segue il flusso documentato da Swift.org per la creazione e l’esecuzione dei pacchetti. Creare uno strumento da riga di comando

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

Sostituisci il sorgente eseguibile generato con il programma completo riportato sotto. A seconda della struttura generata, il file potrebbe chiamarsi Sources/FirstNonNilDemo/FirstNonNilDemo.swift oppure Sources/main.swift. Usa un solo file di ingresso chiamato FirstNonNilDemo.swift; se il modello ha creato main.swift, rinomina quel file prima di aggiungere il programma @main. La guida di Swift documenta che un programma usa main.swift oppure un punto di ingresso @main. Creare uno strumento da riga di comando

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

Esegui il pacchetto dalla sua directory:

swift run FirstNonNilDemo

Per 221, è accettabile sia 221 tiles: 13 rows x 17 columns sia 221 tiles: 17 rows x 13 columns. Per 223, il programma segnala che non esiste alcun fattore negli intervalli forniti. Il testo tiles produce il messaggio relativo all’input. Si tratta di risultati attesi, non della trascrizione di un’esecuzione registrata. I calcoli sono stati verificati separatamente; nell’ambiente di ricerca non era disponibile un compilatore Swift, quindi il programma Swift è stato esaminato ma non eseguito.

Foundation gestisce soltanto la rimozione degli spazi iniziali e finali. Il metodo trimmingCharacters(in:) rimuove i caratteri corrispondenti da entrambe le estremità; non rimuove i caratteri all’interno della stringa. Di conseguenza, 221 diventa 221, mentre 2 21 resta non valido per questo programma. trimmingCharacters(in:)

Perché l’helper ignora nil senza perdere i risultati successivi

Il tipo del risultato delle attività figlie del gruppo è Value?. Ogni attività figlia fornisce quindi un valore utilizzabile oppure un normale risultato vuoto. Il ciclo for await riceve i risultati delle attività figlie completate e il suo binding opzionale interno seleziona solo un candidato non nil.

Se usi direttamente group.next(), ci sono due tipi distinti di assenza. Il tipo restituito documentato è ChildTaskResult?. Con risultati opzionali delle attività figlie, diventa Value??: l’opzionale esterno indica se esiste un altro risultato di un’attività figlia, mentre quello interno indica se quell’attività ha trovato un valore. TaskGroup.next(isolation:)

Questa distinzione spiega un errore comune: prendere un solo risultato e restituirlo immediatamente. Se il primo calcolo completato restituisce nil, altre attività figlie potrebbero ancora avere risposte utili. Il ciclo nell’esempio completo continua a consumare risultati finché non trova un valore o il gruppo non è esaurito.

L’helper usa addTaskUnlessCancelled e controlla il valore Boolean restituito. Apple documenta che questo metodo non crea un’attività figlia se il gruppo è già stato annullato. Interrompere il ciclo di invio evita di continuare a inviare lavoro dopo aver rilevato questa condizione. addTaskUnlessCancelled(priority:operation:)

I calcoli acquisiscono il numero immutabile di tessere e restituiscono le risposte tramite il gruppo. Non aggiornano una variabile condivisa per il vincitore. Il risultato generico deve conformarsi a Sendable, in linea con il vincolo documentato per i gruppi di attività. withTaskGroup(of:returning:isolation:body:)

Perché selezionare un vincitore non significa restituirlo subito

Restituire un valore dalla closure del gruppo seleziona la risposta, ma il withTaskGroup che la racchiude attende comunque il completamento di tutte le attività figlie. Apple documenta esplicitamente che anche le attività figlie annullate devono terminare prima che la funzione possa restituire il risultato. withTaskGroup(of:returning:isolation:body:)

Il blocco defer chiama group.cancelAll() ogni volta che la closure termina. Questo richiede l’annullamento dei calcoli rimanenti. L’annullamento non interrompe calcoli arbitrari: le attività figlie devono collaborare e terminare anticipatamente da sole. TaskGroup

In questo esempio, findFactor controlla Task.isCancelled prima di ogni divisore candidato. Ogni unità di lavoro è breve, quindi si presenta spesso l’occasione di fermarsi. Per un calcolo più grande, inserisci i controlli tra blocchi significativi e di durata limitata. Un controllo soltanto prima di un’operazione lunga e indivisibile lascia che quell’operazione continui fino al termine. La guida del linguaggio Swift descrive l’annullamento come cooperativo e indica la restituzione di nil come una delle risposte supportate. Concorrenza

Se un calcolo concorrente non termina mai e non risponde mai all’annullamento, questo helper strutturato non può garantire una restituzione rapida. La soluzione supportata consiste nel rendere ogni worker limitato o capace di rilevare l’annullamento. Aggiungere un risultato concorrente di timeout non elimina l’obbligo del gruppo di attendere le proprie attività figlie.

L’annullamento del genitore viene controllato prima dell’invio, all’avvio dell’attività figlia, all’interno del ciclo aritmetico e prima di accettare ogni risultato. Questi controlli rilevano l’annullamento in momenti specifici; non creano una regola atomica secondo cui l’annullamento prevale sempre su un valore. Un annullamento che arriva dopo l’ultimo controllo può coincidere con una restituzione riuscita.

Scegliere la regola di selezione giusta e verificare i limiti

L’esempio sintetico offre un test decisionale utile: sia 13 sia 17 soddisfano il contratto dichiarato. Se lo strumento richiede il numero minore di righe, accettare l’uno o l’altro risultato è errato. L’ordine di completamento è utile solo quando tutte le risposte accettate sono intercambiabili per il compito del lettore.

Esigenza del chiamante Approccio appropriato
Qualsiasi valore accettabile non appena terminano i calcoli Usa il ciclo del gruppo di attività mostrato sopra.
Il primo calcolo riuscito in un elenco con priorità fissa Valuta nell’ordine di priorità oppure conserva gli indici e risolvi i candidati precedenti prima di scegliere.
Il risultato più piccolo o con il punteggio migliore Confronta i risultati pertinenti prima di selezionare.
Più risultati noti necessari insieme Valuta async let.

La guida del linguaggio Swift presenta async let per attività figlie note i cui risultati serviranno in seguito e i gruppi di attività per controllare esplicitamente le attività figlie aggiunte dinamicamente. Attendere risultati con nome in un ordine scelto non implementa un ciclo di selezione basato sull’ordine di completamento. Concorrenza

Usa questi controlli quando adatti il programma:

  • Mantieni l’asserzione per l’elenco vuoto: dovrebbe restituire nil.
  • Mantieni l’asserzione mista nil e 42: dovrebbe restituire 42, indipendentemente dall’ordine di completamento.
  • Verifica i calcoli che restituiscono tutti nil.
  • Per più risposte valide, verifica che il risultato appartenga all’insieme dei valori validi invece di imporre un vincitore specifico.
  • Esamina ogni worker per verificare che il progresso sia limitato e che siano presenti controlli dell’annullamento.
  • Indica che la ricerca non è riuscita limitandoti agli intervalli forniti; non dedurre che non esista una risposta al di fuori di essi.

Le tre piccole scansioni dei fattori dimostrano il comportamento, non un miglioramento della velocità. Per questi numeri illustrativi è sufficiente una scansione sequenziale. Usa lo schema dei calcoli concorrenti quando il compito effettivo trae vantaggio dall’accettare risultati intercambiabili e misura quel carico di lavoro prima di decidere quante operazioni eseguire in parallelo.

Rendi esplicita la regola di accettazione, ignora i risultati vuoti delle attività figlie e fai in modo che il lavoro restante possa fermarsi. Così lo strumento per Mac offre un contratto prevedibile per il valore: il primo risultato utilizzabile fornito dal gruppo, seguito dal completamento dell’arresto cooperativo delle attività figlie.

Fonti