cat swift/task-structured-concurrency.md
Task e structured concurrency
Capire cosa crea un Task, perché la structured concurrency è più sicura dei lavori staccati e dove mettere i confini asincroni.
Un Task rappresenta un’unità di lavoro asincrona. In Swift moderno, però, non vogliamo lanciare lavori nel vuoto. Vogliamo che i task abbiano una struttura: un genitore, figli, cancellazione e lifetime comprensibili.
Questa è la structured concurrency.
Creare un Task
Da codice sincrono:
Task {
do {
let prodotti = try await catalogo.carica()
print(prodotti)
} catch {
print(error)
}
}
Questo crea un task asincrono.
Usalo per entrare nel mondo async da:
- azioni UI;
- entry point sincroni;
- callback legacy;
- setup iniziale.
Non usarlo come scorciatoia per evitare di progettare la firma async.
Structured vs unstructured
Una funzione async che chiama altre funzioni async crea un flusso strutturato:
func aggiorna() async throws {
let prodotti = try await api.prodotti()
let evidenza = try await api.inEvidenza()
salva(prodotti, evidenza)
}
Il chiamante aspetta aggiorna. Errori e cancellazione si propagano in modo naturale.
Un Task {} creato dentro una funzione può invece sfuggire:
func aggiorna() {
Task {
try await api.prodotti()
}
}
La funzione ritorna subito. Chi l’ha chiamata non sa quando il lavoro finisce, se fallisce, o se va cancellato.
Regola pratica
Preferisci:
func aggiorna() async throws { ... }
invece di:
func aggiorna() {
Task { ... }
}
Usa Task solo quando devi davvero creare un confine.
Task e priorità
Puoi dare una priorità:
Task(priority: .userInitiated) {
try await caricaDatiVisibili()
}
Le priorità sono suggerimenti al runtime, non garanzie assolute.
Non risolvere problemi di architettura cambiando priorità. Prima organizza il lavoro.
Task.detached
Task.detached crea un task non legato al contesto corrente.
Task.detached {
await lavoroIndipendente()
}
Usalo raramente. Un task detached non eredita allo stesso modo priorità, task local values e isolamento. È più facile creare lavoro difficile da controllare.
Se non hai una ragione forte, non usare detached.
Lifetime del task
Un task dovrebbe vivere quanto il lavoro che rappresenta.
In UI, spesso tieni un riferimento per poter cancellare:
final class CatalogViewModel {
private var loadTask: Task<Void, Never>?
func load() {
loadTask?.cancel()
loadTask = Task {
await carica()
}
}
deinit {
loadTask?.cancel()
}
}
Questo è importante: se l’utente cambia schermata, il lavoro non dovrebbe continuare senza motivo.
Errori nei Task
Se crei un task con errore e non osservi il risultato, puoi perdere informazioni.
Task {
try await api.prodotti()
}
Meglio gestire:
Task {
do {
let prodotti = try await api.prodotti()
await mostra(prodotti)
} catch is CancellationError {
// cancellazione attesa
} catch {
await mostraErrore(error)
}
}
Task.value
Puoi conservare un task che restituisce valore:
let task = Task {
try await api.prodotti()
}
let prodotti = try await task.value
value aspetta il risultato e rilancia eventuali errori.
Checklist
Quando vuoi creare un Task, chiediti:
- sto entrando da codice sincrono?
- chi aspetta il risultato?
- chi gestisce l’errore?
- chi cancella il lavoro?
- il task deve ereditare il contesto corrente?
- posso rendere la funzione
asyncinvece? - sto usando
detachedper una ragione reale?
Esercizio
Prendi una funzione:
func aggiornaCatalogo() {
Task {
...
}
}
Riscrivila in due versioni:
func aggiornaCatalogo() async throws;- wrapper sincrono che crea un
Tasksolo al confine UI.
Spiega quale parte è structured e quale è confine.