Memorizza l’attività del calcolo in un actor prima della prima sospensione, poi consenti ai chiamanti concorrenti di attendere la stessa attività. Questa guida realizza un esempio locale di somma dei quadrati per un’utilità Mac, con regole esplicite per annullamento, errori e pulizia.
Che cosa dovrebbero condividere i chiamanti concorrenti?
Supponiamo che due parti di un’utilità desktop per Mac abbiano bisogno dello stesso riepilogo calcolato: un’anteprima e un pannello dei dettagli. Entrambe richiedono la somma dei quadrati nell’array sintetico [1, 2, 3, 4]. Entrambe dovrebbero ricevere 30, e le richieste che si uniscono alla stessa esecuzione attiva dovrebbero condividere una sola esecuzione.
L’implementazione seguente assegna a ogni istanza del calcolatore un array di input immutabile. Condivide l’attività corrente finché occupa lo slot dell’operazione in corso dell’actor, poi libera lo slot quando un chiamante rileva il completamento. Una richiesta successiva può avviare un altro calcolo. Si tratta di condividere il lavoro in corso; conservare indefinitamente i risultati completati richiederebbe una strategia di memorizzazione nella cache separata.
Il contratto dei valori è volutamente essenziale:
- Accetta al massimo 100.000 numeri interi, ciascuno compreso tra
0e1,000. - Restituisce la somma dei loro quadrati; un array vuoto restituisce
0. - Assegna un identificatore all’esecuzione, così i chiamanti possono distinguere un’esecuzione condivisa da totali semplicemente uguali.
- Propaga gli errori di calcolo ai chiamanti in attesa di quell’esecuzione.
- Consente a un singolo chiamante di annullare la propria attesa senza annullare il worker condiviso.
Il totale massimo consentito è 100,000 × 1,000² = 100,000,000,000, che rientra nel tipo Int a 64 bit del Mac. Questi limiti fanno parte del contratto di questo esempio. Modificarli richiede di rivalutare l’overflow aritmetico e l’uso delle risorse.
Perché un actor ha comunque bisogno di un’attività memorizzata?
Un actor protegge il proprio stato mutabile, ma un suo metodo asincrono può sospendersi. Durante la sospensione, un’altra chiamata può entrare nell’actor. La guida del linguaggio Swift spiega l’isolamento degli actor e i punti di sospensione, mentre la sessione di Apple sugli actor mostra come le ipotesi formulate prima di un await possano diventare obsolete. Concurrency — The Swift Programming Language, Protect mutable state with Swift actors.
Di conseguenza, controllare se esiste già un risultato e poi attendere un nuovo calcolo non impedisce il lavoro duplicato. Un altro chiamante può arrivare prima che il risultato esista. Invece, pubblica sincronicamente il riferimento all’attività: controlla lo slot dell’operazione in corso, crea un worker solo se lo slot è vuoto, memorizzalo e poi attendi il risultato.
Apple documenta che un’attività può iniziare subito dopo la creazione. Accedere al suo value asincrono attende il completamento e propaga gli errori generati. L’actor coordina quindi la proprietà di un solo riferimento; i chiamanti ottengono i risultati tramite quel riferimento. Task, value.
Realizzare l’esempio locale completo
Usa un compilatore Swift 6 su macOS 13 o versioni successive. Il requisito macOS qui deriva dalla chiamata dimostrativa Task.sleep(for:). Questa chiamata si sospende senza bloccare il thread sottostante; la breve pausa rende più facile osservare richieste sovrapposte. Non esegue alcuna parte del calcolo e può essere rimossa in seguito. sleep(for:tolerance:clock:).
Salva il programma completo seguente come SharedCalculation.swift. Usa le impostazioni predefinite ordinarie non isolate per la compilazione da riga di comando mostrata qui sotto. Foundation fornisce UUID, le cui conformità documentate includono Sendable ed Equatable; l’identificatore contrassegna ogni operazione in corso e consente una pulizia condizionale. UUID.
import Foundation
struct CalculationResult: Sendable {
let total: Int
let runID: UUID
}
enum CalculationError: Error {
case invalidInput
}
actor SharedCalculation {
private struct Flight {
let id: UUID
let task: Task<CalculationResult, Error>
}
private let samples: [Int]
private var flight: Flight?
init(samples: [Int]) {
self.samples = samples
}
func value() async throws -> CalculationResult {
try Task.checkCancellation()
let active: Flight
if let existing = flight {
active = existing
} else {
let id = UUID()
let snapshot = samples
let task = Task.detached(priority: .userInitiated) {
[snapshot, id] () async throws -> CalculationResult in
guard snapshot.count <= 100_000,
snapshot.allSatisfy({ (0...1_000).contains($0) })
else {
throw CalculationError.invalidInput
}
// Demonstration only: allow overlap to be observed.
try await Task.sleep(for: .milliseconds(100))
let total = snapshot.reduce(0) { sum, sample in
sum + sample * sample
}
return CalculationResult(total: total, runID: id)
}
active = Flight(id: id, task: task)
flight = active
}
defer {
// An older waiter must not clear a newer flight.
if flight?.id == active.id {
flight = nil
}
}
let result = try await active.task.value
try Task.checkCancellation()
return result
}
}
@main
struct Demo {
static func main() async throws {
let calculation = SharedCalculation(samples: [1, 2, 3, 4])
async let preview = calculation.value()
async let details = calculation.value()
async let inspector = calculation.value()
let (a, b, c) = try await (preview, details, inspector)
precondition(a.total == 30 && b.total == 30 && c.total == 30)
let observedRuns = Set([a.runID, b.runID, c.runID]).count
print("Total: \(a.total)")
print("Runs observed across three requests: \(observedRuns)")
let later = try await calculation.value()
precondition(later.total == 30)
print("Later request started another run: \(later.runID != c.runID)")
}
}
Compilalo ed eseguilo dalla directory che contiene il file:
swiftc -swift-version 6 -parse-as-library SharedCalculation.swift -o SharedCalculation
./SharedCalculation
Le asserzioni aritmetiche dovrebbero avere esito positivo. Quando tutte e tre le chiamate iniziali entrano prima che venga liberata l’operazione in corso, il numero di esecuzioni osservato è 1. La richiesta successiva arriva dopo che quelle chiamate sono terminate, quindi avvia un altro calcolo. La pausa favorisce la sovrapposizione, ma non garantisce la pianificazione: una richiesta iniziale ritardata abbastanza a lungo potrebbe legittimamente avviare un’operazione successiva. Swift non era disponibile nell’ambiente di ricerca, quindi questo programma è stato esaminato ma non compilato né eseguito qui.
Il worker usa un’attività detached perché questo calcolo appartiene al calcolatore condiviso, non a uno specifico chiamante, e la sua aritmetica dovrebbe essere eseguita indipendentemente dall’actor coordinatore. Acquisisce solo l’istantanea dell’input e l’identificatore. Apple consiglia attività figlie strutturate quando possono esprimere la durata richiesta; le attività detached richiedono decisioni esplicite su durata e annullamento. L’esempio specifica anche la priorità. detached(name:priority:operation:).
In che modo la pulizia evita di cancellare il calcolo successivo?
La sequenza di controllo e memorizzazione non contiene await, quindi una seconda chiamata all’actor non può inserirsi tra il controllo dello slot vuoto e la pubblicazione della nuova operazione. Quando il primo chiamante si sospende su active.task.value, un altro chiamante può entrare, trovare quell’operazione e attendere lo stesso riferimento.
La pulizia richiede un proprio controllo d’identità perché più chiamanti possono riprendere dalla stessa attività completata in momenti diversi. Considera questa sequenza:
| Evento | Slot dell’operazione in corso |
|---|---|
| I chiamanti A e B si uniscono all’esecuzione X | X |
| X termina; A riprende e libera X | Vuoto |
| Il chiamante C entra e avvia l’esecuzione Y | Y |
| B riprende da X ed esegue la pulizia | Y deve restare |
Un’assegnazione incondizionata di nil da parte di B cancellerebbe il riferimento a Y. Un quarto chiamante potrebbe quindi avviare un lavoro duplicato mentre Y è ancora in esecuzione. Confrontare flight?.id con active.id rende la pulizia specifica dell’esecuzione a cui ciascun chiamante si è effettivamente unito. Questa sequenza deriva dall’implementazione e dal comportamento di rientranza documentato. Protect mutable state with Swift actors.
Il confine esatto della condivisione è lo slot dell’operazione memorizzata. Un worker potrebbe essere già terminato mentre i chiamanti in attesa non hanno ancora ripreso l’esecuzione per liberare lo slot. Una richiesta che entra in quell’intervallo può ancora ricevere il risultato dell’attività completata. Dopo che lo slot è stato liberato, una nuova richiesta avvia una nuova esecuzione. Questo breve intervallo dopo il completamento fa parte del contratto.
Che cosa succede se un chiamante annulla o il calcolo fallisce?
In Swift l’annullamento è cooperativo: il codice deve decidere dove verificarlo e che cosa fare. Questo esempio controlla l’attività richiedente prima di unirsi e di nuovo dopo aver ricevuto con successo il risultato condiviso. Task — Task Cancellation.
Un chiamante già annullato al primo controllo genera un errore senza avviare né unirsi al lavoro. Un chiamante annullato mentre è in attesa continua ad attendere il worker condiviso; dopo aver ricevuto un risultato positivo dal worker, il secondo controllo genera CancellationError. Gli altri chiamanti possono comunque ricevere il risultato, perché questa implementazione non inoltra al worker l’annullamento di un singolo chiamante.
Questa strategia non consente di interrompere subito l’attesa. Se tutti i chiamanti annullano, il worker termina comunque. È una scelta intenzionale per questo calcolo locale e limitato. Una soluzione che richiedesse l’uscita immediata di ogni singolo chiamante avrebbe bisogno di una gestione aggiuntiva delle attese e di una regola definita chiaramente per stabilire quando fermare il worker condiviso.
Un input non valido fa generare al worker CalculationError.invalidInput. I chiamanti che condividono già quell’attività ricevono l’errore tramite value, e la pulizia differita libera lo slot corrispondente. Una richiesta successiva riprova con lo stesso input immutabile e fallisce di nuovo. Nessun totale viene sostituito in silenzio e nessun elemento non valido viene scartato. value.
C’è anche un dettaglio esplicito sull’ordine degli errori: se un chiamante viene annullato mentre il worker fallisce, l’errore del worker viene propagato prima del secondo controllo dell’annullamento. Se il worker ha successo, il controllo dell’annullamento del chiamante determina se quel chiamante restituisce il risultato.
Come verificare e usare questo schema in un’utilità per Mac?
I totali uguali da soli non dimostrano che l’esecuzione sia condivisa: anche tre calcoli separati di questo input producono 30. Confronta gli identificatori delle esecuzioni per verificare la sovrapposizione e controlla separatamente il risultato numerico.
Per un test di concorrenza deterministico, sostituisci la pausa dimostrativa con un meccanismo asincrono controllabile e registra l’ammissione all’operazione in corso. Mantieni bloccato il worker finché tutti i chiamanti previsti non si sono uniti, poi rilascialo. Verifica l’avvio di un solo worker, identificatori corrispondenti, totali corretti, propagazione degli errori e una nuova esecuzione dopo la pulizia. Prova anche la sequenza di pulizia riportata nella tabella. Aumentare la durata della pausa non può garantire queste condizioni in modo affidabile.
In un’app desktop, mantieni una sola istanza del calcolatore per i componenti che usano lo stesso input. Creare una nuova istanza per ogni pannello assegna a ciascun pannello uno slot indipendente per le operazioni in corso. Quando cambia l’input sintetico, crea un nuovo calcolatore per quell’istantanea; questo esempio non definisce input mutabili né condivisione basata su chiavi.
Applica i valori restituiti allo stato dell’interfaccia utente sul main actor. La guida del linguaggio Swift documenta @MainActor per il lavoro relativo all’interfaccia; il calcolatore condiviso può continuare a coordinare separatamente il proprio stato. Concurrency — The Swift Programming Language.
Per una somma minuscola, creare un’attività è un sovraccarico inutile; l’esempio rende visibili le regole di proprietà, così da poterle applicare a un calcolo locale più sostanzioso. Quando lo adatti, conserva la sequenza essenziale: pubblica un’attività prima della sospensione, attendi quel riferimento e libera solo l’operazione che è terminata.
Fonti
- Concurrency — The Swift Programming Language — Swift.org.
- Task — Documentazione per sviluppatori Apple.
- value — Documentazione per sviluppatori Apple.
- detached(name:priority:operation:) — Documentazione per sviluppatori Apple.
- sleep(for:tolerance:clock:) — Documentazione per sviluppatori Apple.
- UUID — Documentazione per sviluppatori Apple.
- Protect mutable state with Swift actors — Apple Developer.