Mutex e Sync in Go
Il pacchetto sync di Go fornisce gli strumenti fondamentali per la sincronizzazione tra goroutine. Quando piu goroutine accedono a dati condivisi, e necessario proteggerli per evitare race condition e comportamenti imprevedibili.
sync.Mutex
Un Mutex (mutual exclusion) garantisce che solo una goroutine alla volta possa accedere a una sezione critica del codice:
package main
import (
"fmt"
"sync"
)
type ContatoreSicuro struct {
mu sync.Mutex
valore int
}
func (c *ContatoreSicuro) Incrementa() {
c.mu.Lock()
defer c.mu.Unlock()
c.valore++
}
func (c *ContatoreSicuro) Leggi() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.valore
}
func main() {
contatore := &ContatoreSicuro{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
contatore.Incrementa()
}()
}
wg.Wait()
fmt.Println("Valore finale:", contatore.Leggi()) // stampa 1000
}
Senza il Mutex, il risultato sarebbe imprevedibile a causa delle race condition.
sync.RWMutex
Un RWMutex e un mutex che distingue tra letture e scritture. Permette letture concorrenti ma garantisce accesso esclusivo durante le scritture:
package main
import (
"fmt"
"sync"
"time"
)
type Cache struct {
mu sync.RWMutex
dati map[string]string
}
func (c *Cache) Leggi(chiave string) (string, bool) {
c.mu.RLock() // blocco in sola lettura
defer c.mu.RUnlock()
valore, ok := c.dati[chiave]
return valore, ok
}
func (c *Cache) Scrivi(chiave, valore string) {
c.mu.Lock() // blocco esclusivo in scrittura
defer c.mu.Unlock()
c.dati[chiave] = valore
}
func main() {
cache := &Cache{dati: make(map[string]string)}
var wg sync.WaitGroup
// Scrittore
wg.Add(1)
go func() {
defer wg.Done()
cache.Scrivi("lingua", "Go")
}()
time.Sleep(10 * time.Millisecond)
// Lettori concorrenti
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if val, ok := cache.Leggi("lingua"); ok {
fmt.Println("Letto:", val)
}
}()
}
wg.Wait()
}
RWMutex e ideale quando le letture sono molto piu frequenti delle scritture.
sync.WaitGroup
WaitGroup permette di attendere il completamento di un gruppo di goroutine:
package main
import (
"fmt"
"sync"
"time"
)
func compito(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Compito %d: inizio\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Compito %d: completato\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go compito(i, &wg)
}
wg.Wait()
fmt.Println("Tutti i compiti completati")
}
Regola importante: chiamare wg.Add() prima di avviare la goroutine, non al suo interno.
sync.Once
Once garantisce che una funzione venga eseguita una sola volta, indipendentemente da quante goroutine la chiamino:
package main
import (
"fmt"
"sync"
)
var (
istanza *Database
once sync.Once
)
type Database struct {
connessione string
}
func OttieniDatabase() *Database {
once.Do(func() {
fmt.Println("Inizializzazione database...")
istanza = &Database{connessione: "localhost:5432"}
})
return istanza
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
db := OttieniDatabase()
fmt.Println("Connessione:", db.connessione)
}()
}
wg.Wait()
// "Inizializzazione database..." viene stampato una sola volta
}
sync.Once e perfetto per implementare il pattern Singleton in modo thread-safe.
sync.Map
sync.Map e una mappa concorrente ottimizzata per scenari specifici, dove le chiavi sono stabili e le letture predominano:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Scrittura
m.Store("nome", "Go")
m.Store("versione", "1.23")
// Lettura
if val, ok := m.Load("nome"); ok {
fmt.Println("Nome:", val) // stampa "Nome: Go"
}
// LoadOrStore: carica il valore esistente o ne memorizza uno nuovo
valore, caricato := m.LoadOrStore("autore", "Google")
fmt.Println(valore, "gia presente:", caricato)
// Iterazione
m.Range(func(chiave, valore any) bool {
fmt.Printf("%s: %s\n", chiave, valore)
return true // restituire false per interrompere l'iterazione
})
// Eliminazione
m.Delete("autore")
}
Nota: per la maggior parte dei casi d’uso, una mappa normale protetta da un sync.RWMutex e piu performante. Usa sync.Map solo quando le chiavi sono relativamente stabili.
sync.Pool
sync.Pool e un pool di oggetti riutilizzabili, utile per ridurre le allocazioni di memoria:
package main
import (
"fmt"
"sync"
)
var bufferPool = sync.Pool{
New: func() any {
fmt.Println("Creazione nuovo buffer")
return make([]byte, 0, 1024)
},
}
func main() {
// Ottieni un buffer dal pool
buf := bufferPool.Get().([]byte)
buf = append(buf, []byte("ciao mondo")...)
fmt.Println(string(buf))
// Restituisci il buffer al pool (dopo averlo resettato)
buf = buf[:0]
bufferPool.Put(buf)
// Il prossimo Get riutilizzera il buffer esistente
buf2 := bufferPool.Get().([]byte)
fmt.Println("Lunghezza:", len(buf2), "Capacita:", cap(buf2))
}
Operazioni Atomiche con sync/atomic
Il pacchetto sync/atomic fornisce operazioni atomiche a basso livello per tipi primitivi, senza bisogno di mutex:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var contatore atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
contatore.Add(1) // incremento atomico
}()
}
wg.Wait()
fmt.Println("Valore:", contatore.Load()) // stampa 1000
}
A partire da Go 1.19, i tipi atomici (atomic.Int32, atomic.Int64, atomic.Bool, atomic.Pointer[T]) offrono un’interfaccia piu sicura e leggibile rispetto alle funzioni atomic.AddInt64, atomic.LoadInt64, ecc.
var flag atomic.Bool
flag.Store(true)
fmt.Println(flag.Load()) // true
vecchio := flag.Swap(false)
fmt.Println(vecchio) // true
Conclusione
Il pacchetto sync e il sotto-pacchetto sync/atomic forniscono tutti gli strumenti necessari per la sincronizzazione sicura tra goroutine. Mutex e RWMutex proteggono le sezioni critiche, WaitGroup coordina gruppi di goroutine, Once garantisce l’esecuzione unica, e le operazioni atomiche offrono sincronizzazione leggera per tipi primitivi. Scegliere lo strumento giusto dipende dal contesto specifico e dai pattern di accesso ai dati.