cat swift/taskgroup-lavoro-dinamico.md
TaskGroup per lavoro dinamico
I task group servono quando il numero di operazioni concorrenti dipende dai dati: scaricare dettagli, immagini o risorse in parallelo mantenendo struttura.
async let è ottimo per poche operazioni note. Ma cosa fai quando devi caricare i dettagli di 50 prodotti?
for id in ids {
let prodotto = try await api.prodotto(id: id)
}
Questo è sequenziale. Per lavoro dinamico è concorrente, Swift offre i task group.
withThrowingTaskGroup
func caricaProdotti(ids: [String]) async throws -> [Prodotto] {
try await withThrowingTaskGroup(of: Prodotto.self) { group in
for id in ids {
group.addTask {
try await api.prodotto(id: id)
}
}
var prodotti: [Prodotto] = []
prodotti.reserveCapacity(ids.count)
for try await prodotto in group {
prodotti.append(prodotto)
}
return prodotti
}
}
Ogni addTask aggiunge un figlio. Il for try await raccoglie i risultati man mano che arrivano.
Ordine dei risultati
I risultati di un task group arrivano in ordine di completamento, non necessariamente nell’ordine di input.
Se l’ordine conta, restituisci anche l’indice:
func caricaProdottiOrdinati(ids: [String]) async throws -> [Prodotto] {
let elementi = try await withThrowingTaskGroup(of: (Int, Prodotto).self) { group in
for (index, id) in ids.enumerated() {
group.addTask {
let prodotto = try await api.prodotto(id: id)
return (index, prodotto)
}
}
var risultati: [(Int, Prodotto)] = []
for try await risultato in group {
risultati.append(risultato)
}
return risultati
}
return elementi
.sorted { $0.0 < $1.0 }
.map { $0.1 }
}
Cancellazione e errori
Con withThrowingTaskGroup, se un task figlio lancia errore è l’errore viene propagato, gli altri task figli vengono cancellati.
Questo è parte della structured concurrency: il gruppo è uno scope.
TaskGroup non throwing
Se i task non lanciano:
await withTaskGroup(of: Int.self) { group in
for numero in numeri {
group.addTask {
numero * 2
}
}
var risultati: [Int] = []
for await valore in group {
risultati.append(valore)
}
return risultati
}
Limitare parallelismo
Un task group può avviare molti task. Se ids contiene 10.000 elementi, potresti sovraccaricare risorse.
Una strategia semplice è processare a batch:
for batch in ids.chunked(into: 20) {
let prodotti = try await caricaProdotti(ids: batch)
tutti.append(contentsOf: prodotti)
}
Swift standard non include direttamente chunked, ma puoi implementarlo o usare un approcciò simile.
Il punto: concurrency non significa lanciare tutto insieme senza limiti.
Evitare stato condiviso dentro il gruppo
Non modificare una variabile esterna da task figli:
var prodotti: [Prodotto] = []
group.addTask {
let prodotto = try await api.prodotto(id: id)
prodotti.append(prodotto) // problematico
}
I task figli dovrebbero restituire valori. Il task genitore raccoglie i risultati.
Questo riduce data race e rende il codice compatibile con Swift 6.
Checklist
Usa TaskGroup quando:
- il numero di lavori dipende dai dati;
- vuoi raccogliere risultati man mano;
- vuoi figli strutturati;
- vuoi propagazione di errori/cancellazione;
- vuoi evitare task non controllati.
Attenzione a:
- ordine dei risultati;
- parallelismo eccessivo;
- stato condiviso;
- errori parziali;
- cancellazione.
Esercizio
Scrivi:
func caricaDettagliProdotti(ids: [String]) async throws -> [Prodotto]
usando withThrowingTaskGroup.
Poi crea una seconda versione che preserva l’ordine degli ids.
Infine spiega perché non modifichi un array esterno dentro addTask.