En Go, pour synchroniser les goroutines concurrentes, on utilise des structures du paquet sync, les plus courantes étant sync.Mutex, sync.RWMutex et sync.WaitGroup.
sync.Mutex fournit des mécanismes d'exclusion mutuelle lors de l'accès aux données partagées. Ses méthodes sont Lock() (bloque) et Unlock() (débloque).
sync.RWMutex étend les capacités d'un mutex classique : il permet une lecture parallèle, mais une modification exclusive.
sync.WaitGroup est destiné à attendre la fin d'un groupe de goroutines. À l'aide de Add(int), Done() et Wait(), vous gérez le cycle de vie du travail parallèle.
Par exemple :
var mu sync.RWMutex data := 0 // Lecture mu.RLock() fmt.Println(data) mu.RUnlock() // Écriture mu.Lock() data = 42 mu.Unlock()
Subtilités :
Unlock dans un bloc defer pour ne pas oublier de débloquer le mutex même en cas de panique.WaitGroup.Done() correspond à Add().Peut-on acquérir le même sync.Mutex (ou RWMutex) deux fois de suite dans la même goroutine ? Que se passe-t-il ?
Réponse : Non, si vous appelez Lock() sur le même Mutex deux fois de suite sans Unlock() intermédiaire, la goroutine se bloquera indéfiniment (deadlock). En Go, les mutex ne sont pas récursifs.
Exemple :
var mu sync.Mutex mu.Lock() // ... mu.Lock() // DEADLOCK : se bloquera indéfiniment parce que le même thread détient déjà le verrou
Histoire
Dans un projet pour une API REST à fort trafic, un développeur a enveloppé tout le traitement de la requête avec un seul mutex. Cela a entraîné une forte chute des performances — une seule requête pouvait être traitée à la fois, alors que des milliers de clients étaient prévus. La raison était l'ignorance de la différence entre Mutex et RWMutex et l'ignorance des lectures parallèles.
Histoire
Lors de la copie d'une structure contenant un Mutex par un membre de l'équipe, une copie a été accidentellement transmise à une autre fonction. Cela a conduit à un message d'erreur "sync : copie de sync.Mutex" et des crashes en production sous forte charge.
Histoire
Lors de l'utilisation de WaitGroup, on a oublié d'appeler Done() dans plusieurs goroutines, ce qui a entraîné une attente infinie de Wait(), bloquant le thread principal. En conséquence, le service perdait sa disponibilité jusqu'à un redémarrage manuel.