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.Mutex
gestita 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
.