Vai al contenuto
eLearner.app
Modulo 7 · Lezione 4 di 534/50 nel corso~14 min
Lezioni del modulo (4/5)

`sync.Mutex` e `sync.WaitGroup`

I channel sono l'idioma primario di Go, ma a volte servono primitive più tradizionali. Il pacchetto sync fornisce mutex, wait group e altre utilità per coordinare stato condiviso.

sync.Mutex: lock esclusivo

Go
import "sync"

type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *Counter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

Solo una goroutine alla volta entra fra Lock() e Unlock(). Pattern obbligatorio: defer mu.Unlock() subito dopo Lock().

sync.RWMutex: molti lettori, un solo scrittore

Per stato a lettura prevalente:

Go
var mu sync.RWMutex

mu.RLock()              // read lock: più goroutine simultanee OK
v := data
mu.RUnlock()

mu.Lock()               // write lock: esclusivo
data = newValue
mu.Unlock()

Utile solo se le letture dominano davvero le scritture; altrimenti un sync.Mutex normale è più semplice e spesso più veloce per la minore contesa interna.

sync.WaitGroup: aspetta N goroutine

Go
var wg sync.WaitGroup

for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        work(id)
    }(i)
}
wg.Wait()    // blocca finché tutti i Done() sono arrivati

Workflow:

  1. wg.Add(N) aumenta il contatore di N (prima di lanciare le goroutine).
  2. Ogni goroutine chiama wg.Done() quando finisce (idealmente defer).
  3. wg.Wait() blocca finché il contatore non torna a 0.

sync.Once: inizializzazione lazy thread-safe

Go
var (
    once   sync.Once
    config *Config
)

func GetConfig() *Config {
    once.Do(func() {
        config = loadConfig()
    })
    return config
}

Il callback in once.Do viene eseguito esattamente una volta, anche con chiamate concorrenti. Pattern singleton / inizializzazione lazy.

Mutex vs channel: quando usare cosa

Linee guida (anche dall'FAQ ufficiale di Go):

CasoStrumento preferito
Passare ownership di un datochannel
Distribuire lavoro (work queue)channel
Coordinare goroutine indipendentichannel
Proteggere un campo di una structMutex
Cache / contatoreMutex o atomic
Riferimento singletonsync.Once

"Channel per orchestrare, mutex per dati condivisi" è una buona euristica.

Race detector

Esegui i test con -race per scoprire data race:

Bash
go test -race ./...
go run -race main.go

Il race detector instrumenta il binario e logga ogni accesso non sincronizzato a memoria condivisa. È il tool #1 per validare codice concorrente prima della produzione.

Prova tu

Esercizio#go.m7.l4.e1
Tentativi: 0Caricamento…

Proteggi l'incremento di count con mu.Lock() + defer mu.Unlock().

Caricamento editor…
Mostra suggerimento

defer mu.Unlock() subito dopo Lock() garantisce il rilascio anche in caso di panic.

Soluzione disponibile dopo 3 tentativi

Esercizio#go.m7.l4.e2
Tentativi: 0Caricamento…

Lancia 3 goroutine e attendile tutte con sync.WaitGroup (Add(3), Done() in ogni goroutine, Wait()).

Caricamento editor…
Mostra suggerimento

Add PRIMA di lanciare la goroutine; Done DENTRO la goroutine con defer.

Soluzione disponibile dopo 3 tentativi

Quiz#go.m7.l4.e3
Pronto

Qual è il pattern raccomandato per garantire il rilascio del mutex?

Go
mu.Lock()
// ?
Opzioni di risposta

Recap

  • sync.Mutex protegge stato condiviso; sempre defer mu.Unlock().
  • Usa pointer receiver per evitare la copia del mutex.
  • sync.RWMutex per pattern molti-lettori/un-scrittore (solo se utile).
  • sync.WaitGroup: Add prima del go, Done con defer, Wait per aspettare.
  • sync.Once: inizializzazione lazy thread-safe.
  • "Channel per orchestrare, mutex per dati condivisi".
  • go test -race: il tool #1 per scovare data race.