Usa una singola OperationQueue di Foundation di lunga durata e imposta maxConcurrentOperationCount prima di inviare i calcoli. Per un’utilità Mac che riceve una richiesta a ogni pressione di un pulsante, questo limita il lavoro simultaneo consentendo alle richieste successive di restare in attesa.

Questo tutorial realizza il motore di calcolo per questa attività desktop e un programma autonomo per verificarne il comportamento. Ogni richiesta contiene numeri interi sintetici e ogni risultato resta in memoria. Il calcolo viene eseguito in modo sincrono all’interno della sua operazione, quindi la durata dell’operazione copre il lavoro effettivo.

Quale strumento di concorrenza è adatto alle singole richieste in arrivo?

Una OperationQueue accetta operazioni singolarmente e il suo metodo addOperation(_:), basato su blocchi, racchiude ogni chiusura inviata in un’operazione. Conserva la coda in un oggetto che rimane attivo tra una richiesta e l’altra, quindi invia una chiusura per ogni elemento in arrivo. La documentazione di Apple su addOperation(_:) descrive questo metodo di invio.

Imposta maxConcurrentOperationCount su un limite positivo, ad esempio 2. Apple definisce questa proprietà come il numero massimo di operazioni di quella coda che possono essere eseguite simultaneamente. Il limite riguarda quella specifica coda: creare una coda separata per ogni richiesta vanificherebbe il limite condiviso. Se in seguito riduci il limite, le operazioni già in esecuzione continueranno comunque. La documentazione di Apple sui limiti di concorrenza spiega questi vincoli.

Un gruppo di attività Swift ha una durata diversa. Appartiene a un ambito withTaskGroup, non può essere usato al di fuori dell’attività che lo ha creato e attende il completamento delle attività figlie prima di restituire il controllo. Di conseguenza, non è supportato conservare un gruppo di attività come proprietà per future pressioni di pulsanti non correlate. Per questo motore di calcolo sincrono, una coda di operazioni conservata fornisce la durata necessaria e un’impostazione esplicita della concorrenza. La documentazione della libreria standard Swift sui gruppi di attività descrive queste restrizioni.

Organizzazione del lavoro Approccio adatto
I calcoli sincroni indipendenti arrivano singolarmente Conserva una OperationQueue e invia un’operazione per ogni calcolo.
Un’operazione asincrona genitore gestisce una raccolta finita di attività figlie Usa un gruppo di attività nell’ambito di quell’operazione genitore; controlla esplicitamente l’ammissione se serve un limite numerico.
Il lavoro deve aggiornare l’interfaccia desktop Esegui il calcolo separatamente, poi consegna il risultato a codice isolato su MainActor.

L’ultima riga riguarda la consegna dei risultati. La guida del linguaggio Swift indica l’attore principale come luogo per lo stato dell’interfaccia e mostra chiusure esplicitamente isolate su MainActor. Tieni l’aritmetica onerosa fuori da questa fase di consegna. The Swift Programming Language: Concurrency spiega l’isolamento su MainActor.

Definisci il calcolo e il contratto di completamento

L’esempio accetta un identificatore intero e un numero di iterazioni compreso tra 1 e 1_000_000. Per un numero di iterazioni pari a n, somma i quadrati di tutti gli interi da 1 a n. Restituisce l’identificatore insieme a un risultato UInt64. L’identificatore contrassegna l’output, ma non influisce sull’aritmetica.

Questi sono limiti esemplificativi scelti per questo programma. Per l’input massimo accettato, il risultato è 333333833333500000, che rientra in UInt64. La convalida avviene prima dell’invio, quindi una richiesta non valida genera un errore immediato e non crea operazioni in coda. La restrizione sull’input mantiene valido anche l’intervallo del ciclo.

Ogni richiesta accettata produce un risultato memorizzato al termine della relativa operazione. I risultati si accumulano in un array, quindi gli identificatori ripetuti non sovrascrivono i risultati precedenti. La dimostrazione usa identificatori distinti in ordine crescente e ordina per identificatore per rendere l’output leggibile; questo ordine di visualizzazione non implica nulla sull’ordine di esecuzione.

Questo contratto presuppone che la coda resti disponibile e che le sue operazioni vengano completate. L’esempio non espone la cancellazione. Se in seguito aggiungi la cancellazione, servirà un esito separato per le richieste non completate, perché Apple descrive la cancellazione come una richiesta di arresto a cui il lavoro in esecuzione deve rispondere. La documentazione di Apple su OperationQueue descrive la durata delle operazioni e la cancellazione.

Crea un programma completo con un limite condiviso

Usa un Mac con un compilatore Swift 6.2 o successivo e l’SDK macOS disponibile tramite Xcode o i suoi strumenti a riga di comando. Salva il programma completo seguente come main.swift. I comandi qui sotto selezionano esplicitamente un isolamento predefinito non isolato, mantenendo gli helper di calcolo indipendenti dalle impostazioni di isolamento del target dell’app. Swift documenta questa impostazione del compilatore in SE-0466: Control default actor isolation inference.

Il registro dei risultati usa NSLock per proteggere sia il suo array sia i contatori di attività. La sua conformità @unchecked Sendable affida la responsabilità della sincronizzazione all’implementazione: ogni accesso allo stato mutabile acquisisce lo stesso lock. Apple documenta questa responsabilità in Sendable e la protezione delle sezioni critiche in NSLock.

import Foundation

enum InputError: Error {
    case invalidIterations
}

struct Calculation: Sendable {
    let id: Int
    let iterations: Int
}

func sumOfSquares(_ job: Calculation) -> UInt64 {
    var sum: UInt64 = 0
    for integer in 1...job.iterations {
        let value = UInt64(integer)
        sum += value * value
    }
    return sum
}

final class ExecutionLog: @unchecked Sendable {
    private let lock = NSLock()
    private var active = 0
    private var peak = 0
    private var rows: [(id: Int, value: UInt64)] = []

    func begin() {
        lock.lock()
        defer { lock.unlock() }
        active += 1
        peak = max(peak, active)
    }

    func finish(id: Int, value: UInt64) {
        lock.lock()
        defer { lock.unlock() }
        rows.append((id: id, value: value))
        active -= 1
    }

    func snapshot() -> (
        rows: [(id: Int, value: UInt64)], peak: Int
    ) {
        lock.lock()
        defer { lock.unlock() }
        return (rows, peak)
    }
}

final class CalculationPool {
    private let queue = OperationQueue()
    private let log = ExecutionLog()

    init(limit: Int) {
        precondition(limit > 0)
        queue.name = "Synthetic calculations"
        queue.maxConcurrentOperationCount = limit
    }

    func submit(id: Int, iterations: Int) throws {
        guard (1...1_000_000).contains(iterations) else {
            throw InputError.invalidIterations
        }
        let job = Calculation(id: id, iterations: iterations)
        queue.addOperation { [log] in
            log.begin()
            let value = sumOfSquares(job)
            log.finish(id: job.id, value: value)
        }
    }

    // Call only after the producer has stopped submitting work.
    // This blocking method is for the command-line demonstration.
    func finishForDemo() -> (
        rows: [(id: Int, value: UInt64)], peak: Int
    ) {
        queue.waitUntilAllOperationsAreFinished()
        return log.snapshot()
    }
}

@main
struct Demo {
    static func main() throws {
        let limit = 2
        let pool = CalculationPool(limit: limit)

        try pool.submit(id: 1, iterations: 100_000)
        try pool.submit(id: 2, iterations: 200_000)
        try pool.submit(id: 3, iterations: 300_000)

        let report = pool.finishForDemo()
        let rows = report.rows.sorted { $0.id < $1.id }
        precondition(rows.map { $0.id } == [1, 2, 3])
        precondition(rows.map { $0.value } == [
            UInt64(333338333350000),
            UInt64(2666686666700000),
            UInt64(9000045000050000)
        ])
        precondition(report.peak >= 1 && report.peak <= limit)

        for row in rows {
            print("Job \(row.id): \(row.value)")
        }
        print("Observed peak: \(report.peak)")
    }
}

Compilalo ed eseguilo dalla directory che contiene main.swift:

swiftc -swift-version 6 -default-isolation nonisolated \
  -parse-as-library main.swift -o calculation-demo
./calculation-demo

Le tre righe dei risultati dovrebbero essere:

Job 1: 333338333350000
Job 2: 2666686666700000
Job 3: 9000045000050000

L’ultima riga riporta un picco osservato pari a 1 oppure 2. Un limite consente la sovrapposizione, ma non la garantisce. Le somme attese sono state verificate in modo indipendente usando l’aritmetica intera e la formula n * (n + 1) * (2 * n + 1) / 6. Il programma Swift è stato esaminato, ma non è stato possibile compilarlo nell’ambiente di ricerca perché non era installato alcun eseguibile Swift.

Perché questo limite copre il calcolo effettivo?

Ogni operazione avvia il contatore di attività, esegue l’intero calcolo in modo sincrono, registra il risultato e poi termina. Da quella chiusura non viene avviato alcun ulteriore calcolo asincrono. Pertanto, limitare le operazioni in esecuzione limita anche questi calcoli.

Mantieni questo rapporto quando adatti il programma. Se un’operazione si limita a creare una Task separata e restituisce immediatamente il controllo, la coda considera terminata l’operazione mentre l’attività può continuare a calcolare. Il limite sulle operazioni regolerebbe quindi solo le brevi chiusure di avvio. Questo deriva dalla durata documentata delle operazioni e dall’API di invio basata su blocchi. La panoramica di Apple su OperationQueue e addOperation(_:) definiscono questi aspetti.

Il lock del registro protegge solo brevi sezioni di gestione dei dati. Mantenerlo durante sumOfSquares(_:) serializzerebbe l’accesso al calcolo e impedirebbe la sovrapposizione prevista. Ogni acquisizione e rilascio del lock avviene all’interno di un singolo metodo sincrono, in conformità al requisito di Apple secondo cui un NSLock deve essere sbloccato sul thread che lo ha bloccato. La documentazione di Apple su NSLock spiega questo requisito.

Per verificare un adattamento, esegui le stesse tre richieste con limit impostato su 1, quindi ripristina 2. Entrambe le esecuzioni devono conservare tutti e tre i risultati esatti. L’esecuzione seriale dovrebbe riportare un picco di 1; la seconda esecuzione deve restare entro 2. Questi controlli verificano la conservazione dei risultati e il limite osservato sui calcoli senza presupporre un ordine di pianificazione specifico.

Collega le azioni desktop in arrivo senza bloccare l’interfaccia

Conserva una singola CalculationPool nel componente che gestisce la funzionalità di calcolo desktop. Ogni azione in arrivo chiama submit(id:iterations:) una volta. Evita di ricreare il pool nel gestore dell’azione, perché ogni nuova istanza ha una coda e un limite indipendenti.

La dimostrazione chiama finishForDemo() solo dopo l’ultimo invio, così il processo a riga di comando può raccogliere i risultati prima di uscire. Questo metodo blocca il thread chiamante. In un’app desktop, usa la consegna dei singoli risultati invece di chiamarlo da un gestore di pulsante o dall’attore principale. Apple documenta esplicitamente il comportamento bloccante di waitUntilAllOperationsAreFinished().

Per l’integrazione con l’interfaccia, estendi l’API di invio con una callback per i risultati isolata su MainActor e contrassegnata con @Sendable. Dopo aver calcolato un risultato all’interno dell’operazione, pianifica una Task con una chiusura esplicitamente marcata @MainActor per invocare la callback. Acquisisci il valore immutabile del risultato e la callback. Mantieni solo l’aggiornamento della visualizzazione all’interno di quella chiusura; lascia l’aritmetica nell’operazione. Swift documenta sia la sintassi delle chiusure su MainActor sia le acquisizioni delle chiusure sendable.

Questa versione per l’interfaccia richiede un’ulteriore definizione di completamento: terminare il calcolo e visualizzarne il risultato sono eventi separati. Il programma a riga di comando attende i calcoli e la memorizzazione sincrona dei risultati. Una callback dell’interfaccia pianificata separatamente può essere eseguita in seguito.

Cosa succede quando gli invii superano la capacità di calcolo?

L’impostazione della concorrenza limita le operazioni in esecuzione, mentre il lavoro inviato in eccesso può restare in coda. Inoltre, l’array di risultati completati dell’esempio può continuare a crescere. Di conseguenza, questo esempio limita la concorrenza dei calcoli, ma non la memoria totale né la dimensione della coda. Apple documenta che la coda conserva le operazioni fino al loro completamento. La panoramica di OperationQueue spiega questa conservazione.

Se la funzionalità desktop può generare raffiche prolungate di richieste, scegli una politica di ammissione prima di estenderla: accetta solo un numero fisso di richieste in sospeso e segnala il rifiuto, sospendi gli invii oppure sostituisci un’anteprima sintetica in attesa con una richiesta più recente quando questo comportamento è esplicitamente previsto. Coordina i conteggi di ammissione e gli aggiornamenti di completamento tramite un unico componente sincronizzato. Il solo limite sul lavoro in esecuzione non può attuare queste politiche.

Per le singole richieste sincrone, il progetto essenziale è semplice: conserva una coda, configura una volta il suo limite, mantieni ogni calcolo all’interno della relativa operazione e sincronizza i risultati condivisi. Mantieni questi confini mentre colleghi le azioni desktop, poi aggiungi una politica esplicita per la coda solo se il ritmo delle richieste lo richiede.

Fonti