Go by Example - IT: Mutex

Nell’esempio precedente abbiamo visto come gestire semplici addizioni utilizzando delle operazioni atomiche. Per una gestione dello stato più complessa usiamo le mutex per accedere con sicurezza a dati condivisi attraverso multiple goroutine.

package main
import (
    "fmt"
    "math/rand"
    "runtime"
    "sync"
    "sync/atomic"
    "time"
)
func main() {

Per il nostro esempio lo stato sarà una mappa.

    var stato = make(map[int]int)

Questa mutex sincronizzerà l’accesso a stato.

    var mutex = &sync.Mutex{}

Per confrontare l’approccio basato sulle mutex piuttosto che altri, ops conterà quante operazioni effettuiamo su stato.

    var ops int64 = 0

Di seguito facciamo partire 100 goroutine che dovranno eseguire ripetute operazioni di lettura sullo stato.

    for r := 0; r < 100; r++ {
        go func() {
            totale := 0
            for {

Per ogni lettura scegliamo una chiave per accedere alla map, facciamo un Lock() sulla mutex per avere controllo esclusivo sullo stato, leggiamo il valore alla chiave scelta, sblocchiamo (Unlock()) la mutex, e incrementiamo il numero di operazioni (ops).

                chiave := rand.Intn(5)
                mutex.Lock()
                totale += stato[chiave]
                mutex.Unlock()
                atomic.AddInt64(&ops, 1)

Per essere sicuri che questa goroutine non blocchi lo scheduler, cediamo esplicitamente il controllo dopo ogni operazione utilizzando runtime.Gosched(). In genere viene eseguito automaticamente ad esempio con le operazioni sui channel o con funzioni come time.Sleep, ma in questo caso abbiamo bisogno di farlo manualmente.

                runtime.Gosched()
            }
        }()
    }

Faremo inoltre partire altre 10 goroutine per simulare la scrittura, con lo stesso pattern che abbiamo usato per la lettura.

    for w := 0; w < 10; w++ {
        go func() {
            for {
                chiave := rand.Intn(5)
                valore := rand.Intn(100)
                mutex.Lock()
                stato[chiave] = valore
                mutex.Unlock()
                atomic.AddInt64(&ops, 1)
                runtime.Gosched()
            }
        }()
    }

Lasciamo ora lavorare le 10 goroutine per un secondo.

    time.Sleep(time.Second)

Prendiamo e stampiamo il numero di operazioni finale.

    opsFinal := atomic.LoadInt64(&ops)
    fmt.Println("ops:", opsFinal)

Con un lock finale sullo stato, vediamo come si è evoluto lo stato.

    mutex.Lock()
    fmt.Println("stato:", stato)
    mutex.Unlock()
}

Eseguendo il nostro programma vedremo che abbiamo eseguito circa 4,000,000 di operazioni sul nostro stato sincronizzato da mutex.

$ go run mutexes.go
ops: 4272750
stato: map[4:29 3:50 0:39 1:71 2:51]

Come prossima cosa, vedremo come come implementare questa gestione della memoria condivisa usando esclusivamente goroutine e channel.

Prossimo esempio: Gestione dello Stato con Goroutine.