Le Go par l'exemple: Mutexes

Dans l’exemple précédent nous avons vu comment gérer des compteurs d’état simples avec des opérations atomiques. Pour des états plus compliqués, on peut utiliser un _mutex_ pour accéder de manière sûre à des données à travers plusieurs goroutines.

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

Pour notre exemple, l’état state sera une map.

    var state = make(map[int]int)

Ce mutex va synchroniser l’accès à state.

    var mutex = &sync.Mutex{}

Pour comparer l’approche avec des mutexes avec une autre que nous verrons plus tard, ops va compter combien d’opérations nous réalisons avec l’état.

    var ops int64 = 0

Ici on lance 100 goroutines pour exécuter des lectures répétées sur l’état.

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

A chaque lecture, on sélectionne une clé à laquelle on souhaite accéder, on bloque le mutex avec Lock() pour s’assurer un accès exclusif à l’état, on lit la valeur de la clé choisie, on débloque le mutex, puis on incrémente le compteur.

                key := rand.Intn(5)
                mutex.Lock()
                total += state[key]
                mutex.Unlock()
                atomic.AddInt64(&ops, 1)

Pour nous assurer que cette goroutine ne prend pas toutes les ressources, on rend la main explicitement après chaque opération avec runtime.Gosched(). Le programmateur gère normalement automatiquement ceci, par ex. après les opérations sur les canaux et pour les appels bloquants comme time.Sleep, mais dans ce cas on doit le faire manuellement.

                runtime.Gosched()
            }
        }()
    }

On démarre également 10 goroutines pour simuler des écritures, de la même manière que pour les écritures.

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

On fait travailler les 10 goroutines sur les state et mutex pendant une seconde.

    time.Sleep(time.Second)

On rapporte le nombre total d’opérations réalisées.

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

Avec un verrou final sur le mutex de state, on peut connaitre l’état final.

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

Lancer le programme montre qu’on a exécuté environ 3,500,000 d’opérations sur state, synchronisées par un mutex

$ go run mutexes.go
ops: 3598302
state: map[1:38 4:98 2:23 3:85 0:44]

Ensuite nous verrons comment implémenter la même gestion d’état avec uniquement des goroutines et des canaux.

Exemple suivant: Goroutines à états.