cat swift/sendable-data-race-safety.md

Lezione 36 55 min

Sendable e data-race safety

Swift 6 porta la sicurezza dai data race nel linguaggio: Sendable descrive valori sicuri da attraversare tra domini concorrenti.

Swift 6 ha reso la data-race safety una parte centrale del linguaggio. L’obiettivo è far emergere potenziali accessi concorrenti non sicuri prima della produzione.

Uno dei concetti chiave è Sendable.

Cosa significa Sendable

Un tipo Sendable può essere trasferito in modo sicuro tra domini concorrenti.

Molti value types immutabili sono naturalmente sendable:

struct Prodotto: Sendable {
  let id: String
  let nome: String
  let prezzoInCent: Int
}

Se tutte le proprietà sono Sendable, Swift può spesso aiutare con la conformità.

Perché serve

Immagina di passare un valore a un task concorrente:

Task {
  await processa(prodotto)
}

Swift vuole sapere se quel valore può attraversare quel confine senza introdurre accessi concorrenti pericolosi.

Un value type immutabile è di solito sicuro. Una classe mutabile condivisa no.

Classe non Sendable

final class SessioneUtente {
  var token: String

  init(token: String) {
    self.token = token
  }
}

Questa classe è mutabile. Passarla tra task può essere rischioso.

Alternative:

  • renderla immutabile;
  • isolarla in un actor;
  • proteggerla con sincronizzazione;
  • evitare di condividerla.

Actor e Sendable

Gli actor sono pensati per accesso concorrente sicuro al loro stato isolato.

actor TokenStore {
  private var token: String?

  func get() -> String? {
    token
  }

  func set(_ token: String?) {
    self.token = token
  }
}

Invece di condividere una classe mutabile, isoli lo stato.

@Sendable closure

Una closure @Sendable deve catturare valori in modo sicuro per la concurrency.

func esegui(_ operation: @Sendable @escaping () async -> Void) {
  Task {
    await operation()
  }
}

Se la closure cattura stato mutabile non sicuro, Swift può avvisarti o bloccare la compilazione in modalità Swift 6.

Non usare unchecked alla leggera

Puoi vedere:

final class LegacyCache: @unchecked Sendable {
  ...
}

@unchecked Sendable dice al compilatore: fidati, ho garantito io la sicurezza.

Usalo solo se hai davvero sincronizzazione interna corretta. Non è un silenziatore da usare per far sparire errori.

Value type progettati bene

Questo è un buon candidato:

struct DettaglioProdotto: Sendable {
  let prodotto: Prodotto
  let recensioni: [Recensione]
}

Se Prodotto e Recensione sono Sendable, anche il dettaglio può esserlo.

Errori comuni

var cache: [String: Prodotto] = [:]

Task {
  cache["p-1"] = prodotto
}

Stai catturando e modificando stato mutabile da un task. In Swift 6 questo tipo di situazione viene diagnosticato molto più severamente.

Meglio:

actor ProductCache {
  private var storage: [String: Prodotto] = [:]

  func set(_ prodotto: Prodotto, for id: String) {
    storage[id] = prodotto
  }
}

Sendable non significa thread-safe generico

Sendable non rende magicamente ogni operazione sicura. È un contratto sul trasferimento tra domini concorrenti.

Se hai stato mutabile condiviso, devi comunque usare isolamento: actor, MainActor, mutex o altri strumenti corretti.

Checklist

Per Swift 6 concurrency:

  • preferisci value types immutabili per dati trasferiti;
  • marca modelli di dominio come Sendable quando ha senso;
  • evita classi mutabili condivise;
  • usa actor per stato condiviso;
  • tratta @unchecked Sendable come ultima risorsa;
  • rendi closure @Sendable quando attraversano confini concorrenti;
  • non catturare variabili mutabili locali in task concorrenti.

Esercizio

Rendi Sendable:

  • Prodotto;
  • Recensione;
  • DettaglioProdotto;
  • APIError.

Poi prova a passare una classe mutabile SessioneUtente dentro un Task e spiega perché è problematica.

Infine sostituiscila con un actor TokenStore.