cat swift/async-let-parallelismo-locale.md

Lezione 3140 min

async let per parallelismo locale

Quando due operazioni asincrone sono indipendenti, async let permette di avviarle insieme mantenendo struttura e leggibilità.

Molto codice asincrono è sequenziale per abitudine, non per necessità.

let prodotto = try await api.prodotto(id: id)
let recensioni = try await api.recensioni(id: id)
let suggeriti = try await api.prodottiSuggeriti(id: id)

Se queste tre operazioni sono indipendenti, stai aspettando inutilmente.

async let permette di avviare lavori asincroni in parallelo, dentro uno scope strutturato.

Forma base

async let prodotto = api.prodotto(id: id)
async let recensioni = api.recensioni(id: id)
async let suggeriti = api.prodottiSuggeriti(id: id)

let dettaglio = try await DettaglioProdotto(
  prodotto: prodotto,
  recensioni: recensioni,
  suggeriti: suggeriti
)

Le operazioni partono quando dichiari gli async let. I risultati vengono attesi quando li usi con await.

Con throws

Se le funzioni possono fallire:

async let prodotto = api.prodotto(id: id)
async let recensioni = api.recensioni(id: id)

return try await DettaglioProdotto(
  prodotto: prodotto,
  recensioni: recensioni
)

Il try await raccoglie eventuali errori.

Scope strutturato

Gli async let sono figli del task corrente. Non scappano dallo scope.

Questo significa:

  • se la funzione viene cancellata, i figli vengono cancellati;
  • se esci dallo scope, Swift deve gestire i task figli;
  • il lifetime resta comprensibile.

Questo è molto diverso da creare task non strutturati.

Quando usare async let

Usalo quando:

  • hai poche operazioni;
  • il numero di task è noto a compile-time o comunque fisso;
  • le operazioni sono indipendenti;
  • vuoi risultati tipizzati e leggibili;
  • vuoi mantenere codice locale.

Esempio:

func dashboard() async throws -> Dashboard {
  async let profilo = api.profilo()
  async let ordini = api.ordiniRecenti()
  async let coupon = api.couponDisponibili()

  return try await Dashboard(
    profilo: profilo,
    ordini: ordini,
    coupon: coupon
  )
}

Quando non usarlo

Non usarlo quando:

  • il numero di task dipende da una collection;
  • devi aggiungere task in un loop dinamico;
  • vuoi limitare parallelismo;
  • devi raccogliere risultati man mano.

In quei casi usa TaskGroup.

Dipendenze tra operazioni

Non parallelizzare ciò che dipende.

let utente = try await api.utente()
async let ordini = api.ordini(utenteID: utente.id)
async let coupon = api.coupon(utenteID: utente.id)

Qui prima serve l’utente; poi ordini e coupon possono partire insieme.

Errore comune: await troppo presto

Questo non parallelizza davvero:

async let prodotto = api.prodotto(id: id)
let prodottoValue = try await prodotto

async let recensioni = api.recensioni(id: id)
let recensioniValue = try await recensioni

Hai atteso il primo prima di avviare il secondo.

Avvia prima, attendi dopo:

async let prodotto = api.prodotto(id: id)
async let recensioni = api.recensioni(id: id)

let result = try await (prodotto, recensioni)

Checklist

Prima di usare async let, chiediti:

  • le operazioni sono indipendenti?
  • il numero di operazioni è piccolo e noto?
  • posso avviarle prima e attenderle dopo?
  • cosa succede se una fallisce?
  • la cancellazione deve propagarsi?

Esercizio

Scrivi:

func caricaHome() async throws -> HomeModel

che carica in parallelo:

  • profilo utente;
  • ordini recenti;
  • coupon disponibili.

Poi modifica l’esercizio: prima carica l’utente, poi usa il suo id per caricare ordini e coupon in parallelo.