cat swift/async-await-sospendere-senza-bloccare.md

Lezione 29 45 min

Async e await: sospendere senza bloccare

La base della concurrency moderna in Swift: scrivere codice asincrono leggibile, sapendo cosa significa sospendere una funzione.

async e await permettono di scrivere codice asincrono con una forma lineare. La cosa importante è capire che await non significa “blocca il thread”. Significa: questa funzione può sospendersi mentre aspetta un risultato.

Questo cambia il modo in cui scrivi networking, lettura file, operazioni lente, chiamate a database, cache remote e UI.

Il problema delle callback

Prima di async/await, molto codice asincrono era scritto con callback:

func caricaProdotto(id: String, completion: @escaping (Result<Prodotto, Error>) -> Void) {
  api.fetchProduct(id: id) { result in
    completion(result)
  }
}

Con molte operazioni concatenate, le callback diventano annidate e difficili da leggere.

Funzione async

Una funzione asincrona dichiara async:

func caricaProdotto(id: String) async throws -> Prodotto {
  try await api.fetchProduct(id: id)
}

La firma dice:

  • può sospendersi (async);
  • può fallire (throws);
  • restituisce un Prodotto.

La chiamata richiede await:

let prodotto = try await caricaProdotto(id: "p-1")

await come punto di sospensione

Ogni await è un punto in cui la funzione può sospendersi.

let prodotto = try await repository.prodotto(id: id)
let recensioni = try await repository.recensioni(per: prodotto.id)

Queste due operazioni sono sequenziali: la seconda parte dopo la prima.

Questo è perfetto se recensioni dipende dal prodotto. Se invece due operazioni sono indipendenti, useremo async let o task group.

Sequenziale non significa sbagliato

Non trasformare tutto in parallelo.

func dettaglioProdotto(id: String) async throws -> DettaglioProdotto {
  let prodotto = try await api.prodotto(id: id)
  let disponibilita = try await api.disponibilita(per: prodotto.id)

  return DettaglioProdotto(prodotto: prodotto, disponibilita: disponibilita)
}

Qui la disponibilità dipende dal prodotto. La sequenza è corretta.

async throws

async e throws si combinano bene:

func caricaCatalogo() async throws -> [Prodotto] {
  let data = try await client.get("/products")
  return try decoder.decode([Prodotto].self, from: data)
}

La chiamata usa try await:

do {
  let prodotti = try await caricaCatalogo()
  print(prodotti)
} catch {
  print("Errore: \(error)")
}

L’ordine è try await, non await try.

Funzioni async e chiamanti

Una funzione che usa await deve essere a sua volta in un contesto asincrono:

func aggiornaCatalogo() async {
  do {
    let prodotti = try await caricaCatalogo()
    print(prodotti)
  } catch {
    print(error)
  }
}

Da codice sincrono puoi creare un Task, ma non abusarne:

Task {
  await aggiornaCatalogo()
}

Il Task è una porta tra mondo sincrono e asincrono. Va usato nei confini, non ovunque.

URLSession

Con Foundation moderna:

func getData(from url: URL) async throws -> Data {
  let (data, response) = try await URLSession.shared.data(from: url)

  guard let http = response as? HTTPURLResponse,
        (200..<300).contains(http.statusCode) else {
    throw APIError.rispostaNonValida
  }

  return data
}

Il codice è lineare: chiamata, validazione, ritorno.

Non bloccare dentro async

Non usare Thread.sleep in codice asincrono. Blocca un thread.

Preferisci:

try await Task.sleep(for: .seconds(1))

Questa forma sospende il task.

Checklist

Ricorda:

  • async indica che una funzione può sospendersi;
  • await marca un punto di sospensione;
  • await non dovrebbe essere nascosto;
  • try await combina errori e asincronia;
  • sequenziale va bene quando c’è dipendenza;
  • crea Task ai confini tra sync e async;
  • non bloccare thread dentro funzioni async.

Esercizio

Scrivi:

func caricaProdotto(id: String) async throws -> Prodotto
func caricaRecensioni(prodottoID: String) async throws -> [Recensione]
func dettaglioProdotto(id: String) async throws -> DettaglioProdotto

La terza funzione deve chiamare le prime due in modo sequenziale, spiegando perché la seconda dipende dal risultato della prima.