Mutex
Mentre la gestione atomica serve per accedere in modo esclusivo a variabili, un mutex (mutual exclusion lock) serve a garantire che il codice delimitato dalle funzioni Lock e Unlock sia eseguito da una sola goroutine.
Tale codice delimitato si chiama sezione critica ed è opportuno che sia la più breve possibile.
Un mutex è una struttura sync.Mutexgestita per riferimento.
Il codice &sync.Mutex{} la crea e ne restituisce l'indirizzo.
Esempio
(370mutexes.go):
package main
import (
"fmt"
"math/rand"
"runtime"
"sync"
"sync/atomic"
"time"
)
func main() {
// Rappresentiamo lo stato con una map
var state = make(map[int]int)
// Variabile per la sincronizzazione dell'accesso
var mutex = &sync.Mutex{}
// Variabile per contare le operazioni eseguite
var ops int64 = 0
// 100 goroutine simultanee per leggere lo stato
for r := 0; r < 100; r++ {
go func() {
// Una variabile per il totale
total := 0
for {
// Scelta di una chiave a caso
key := rand.Intn(5)
// Blocco col mutex
mutex.Lock()
// Incremento del totale col valore
// dell'elemento della mappa
total += state[key]
// Sblocco col mutex
mutex.Unlock()
// Incremento del contatore
atomic.AddInt64(&ops, 1)
// Ripassa il controllo allo schedulatore
runtime.Gosched()
}
}()
}
// 10 goroutine che scrivono valori casuali nella mappa
for w := 0; w < 10; w++ {
go func() {
for {
// Selezione casuale della chiave
key := rand.Intn(5)
// Selezione casuale del valore da scriverci
val := rand.Intn(100)
// Blocco col mutex
mutex.Lock()
// Scrittura del valore
state[key] = val
// Sblocco col mutex
mutex.Unlock()
// Incremento atomico delle operazioni
atomic.AddInt64(&ops, 1)
// Ripassa il controllo allo schedulatore
runtime.Gosched()
}
}()
}
// Lascia lavorare per un secondo
time.Sleep(time.Second)
// Copia atomica del valore di ops e stampa
opsFinal := atomic.LoadInt64(&ops)
fmt.Println("ops:", opsFinal)
// Stampa dell'intera mappa con blocco col mutex
mutex.Lock()
fmt.Println("state:", state)
mutex.Unlock()
}
E' molto comune che lo stato di un programma sia rappresentato tramite una map.
Senza mutex
Per vedere qual'è il comportamento senza l'uso del mutex commentiamo le linee che lo usano.
(371-no-mutex.go):
package main
import (
"fmt"
"math/rand"
"runtime"
// "sync"
"sync/atomic"
"time"
)
func main() {
// Rappresentiamo lo stato con una map
var state = make(map[int]int)
// Variabile per la sincronizzazione dell'accesso
// var mutex = &sync.Mutex{}
// Variabile per contare le operazioni eseguite
var ops int64 = 0
// 2 goroutine simultanee per leggere lo stato
for r := 0; r < 2; r++ {
go func() {
// Una variabile per il totale
total := 0
for {
// Scelta di una chiave a caso
key := rand.Intn(5)
// Blocco col mutex
// mutex.Lock()
// Incremento del totale col valore
// dell'elemento della mappa
total += state[key]
// Sblocco col mutex
// mutex.Unlock()
// Incremento del contatore
atomic.AddInt64(&ops, 1)
// Ripassa il controllo allo schedulatore
runtime.Gosched()
}
}()
}
// 2 goroutine che scrivono valori casuali nella mappa
for w := 0; w < 2; w++ {
go func() {
for {
// Selezione casuale della chiave
key := rand.Intn(5)
// Selezione casuale del valore da scriverci
val := rand.Intn(100)
// Blocco col mutex
// mutex.Lock()
// Scrittura del valore
state[key] = val
// Sblocco col mutex
// mutex.Unlock()
// Incremento atomico delle operazioni
atomic.AddInt64(&ops, 1)
// Ripassa il controllo allo schedulatore
runtime.Gosched()
}
}()
}
// Lascia lavorare per un secondo
time.Sleep(time.Second)
// Copia atomica del valore di ops e stampa
opsFinal := atomic.LoadInt64(&ops)
fmt.Println("ops:", opsFinal)
// Stampa dell'intera mappa con blocco col mutex
// mutex.Lock()
fmt.Println("state:", state)
// mutex.Unlock()
}
Go ha un modulo runtime di detezione di condizioni di corsa, che impedisce la scrittura simultanea di una variabile da parte di due goroutines. Viene caricato con l'opzione -race.