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.