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
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:
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
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 arrivatiWorkflow:
wg.Add(N)aumenta il contatore di N (prima di lanciare le goroutine).- Ogni goroutine chiama
wg.Done()quando finisce (idealmentedefer). wg.Wait()blocca finché il contatore non torna a 0.
sync.Once: inizializzazione lazy thread-safe
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):
| Caso | Strumento preferito |
|---|---|
| Passare ownership di un dato | channel |
| Distribuire lavoro (work queue) | channel |
| Coordinare goroutine indipendenti | channel |
| Proteggere un campo di una struct | Mutex |
| Cache / contatore | Mutex o atomic |
| Riferimento singleton | sync.Once |
"Channel per orchestrare, mutex per dati condivisi" è una buona euristica.
Race detector
Esegui i test con -race per scoprire data race:
go test -race ./...
go run -race main.goIl 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
Proteggi l'incremento di count con mu.Lock() + defer mu.Unlock().
Mostra suggerimento
defer mu.Unlock() subito dopo Lock() garantisce il rilascio anche in caso di panic.
Soluzione disponibile dopo 3 tentativi
Lancia 3 goroutine e attendile tutte con sync.WaitGroup (Add(3), Done() in ogni goroutine, Wait()).
Mostra suggerimento
Add PRIMA di lanciare la goroutine; Done DENTRO la goroutine con defer.
Soluzione disponibile dopo 3 tentativi
Qual è il pattern raccomandato per garantire il rilascio del mutex?
mu.Lock()
// ?Recap
sync.Mutexprotegge stato condiviso; sempredefer mu.Unlock().- Usa pointer receiver per evitare la copia del mutex.
sync.RWMutexper pattern molti-lettori/un-scrittore (solo se utile).sync.WaitGroup:Addprima delgo,Donecondefer,Waitper aspettare.sync.Once: inizializzazione lazy thread-safe.- "Channel per orchestrare, mutex per dati condivisi".
go test -race: il tool #1 per scovare data race.