00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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.