Protéger des données partagées
Un sync.Mutex ne laisse entrer qu'une goroutine à la fois dans le code situé entre Lock et Unlock. Placez le mutex à côté des données qu'il protège, généralement dans la même struct :
Ce code affiche toujours hits: 10000. Sans le verrou, 100 goroutines qui écrivent dans la même map feraient généralement planter le programme avec fatal error: concurrent map writes. Cette erreur vient d'une vérification au mieux du runtime, elle ne peut pas être récupérée, et l'absence de plantage ne prouve pas que le code est correct.
Les détails qui comptent :
- La valeur zéro de
sync.Mutexest déverrouillée et prête. Pas de constructeur. - Les méthodes utilisent un receveur pointeur (
*Counter). Un receveur valeur verrouillerait une copie du mutex, ce qui ne protège rien. defer c.mu.Unlock()juste aprèsLockgarantit que chaque chemin de retour, et un panic, libère le verrou.- Chaque accès passe par le verrou, lectures comprises. Une lecture sans verrou pendant qu'une autre goroutine écrit reste une data race.
Gardez la section critique courte
defer déverrouille à la fin de la fonction. C'est adapté aux méthodes courtes comme celles ci-dessus. Dans une fonction plus longue, déverrouillez dès que les données partagées ne sont plus touchées, pour que les autres goroutines n'attendent pas un travail qui n'a pas besoin du verrou :
func (s *Store) Save(key string) error {
s.mu.Lock()
data := s.items[key] // copy what you need
s.mu.Unlock()
return writeToDisk(key, data) // slow I/O, outside the lock
}
Garder un verrou pendant des appels réseau, des E/S disque ou un envoi sur un channel est la cause la plus courante d'un programme concurrent lent, et un envoi sur un channel sous verrou est une cause fréquente de deadlock.
RWMutex pour les données surtout lues
sync.RWMutex a deux modes. RLock/RUnlock prennent un verrou de lecture partagé que de nombreuses goroutines peuvent détenir en même temps. Lock/Unlock prennent le verrou d'écriture exclusif, qui attend que tous les lecteurs soient sortis.
RWMutex est rentable quand les lectures dominent et que chaque lecture fait un vrai travail sous le verrou. Pour de minuscules sections critiques comme une simple recherche dans une map, un Mutex ordinaire est souvent aussi rapide, parce que le verrou de lecture a sa propre comptabilité. Faites un benchmark avant de choisir.
Vous ne pouvez pas promouvoir un verrou de lecture en verrou d'écriture. Appeler Lock en détenant RLock dans la même goroutine provoque un deadlock. Relâchez d'abord le verrou de lecture, puis prenez le verrou d'écriture et vérifiez à nouveau la condition, car un autre écrivain a pu modifier les données entre-temps.
sync/atomic pour des valeurs uniques
Pour un seul compteur ou flag, sync/atomic est plus simple et moins coûteux qu'un mutex. Les types enveloppes (Go 1.19) sont ceux à utiliser :
Les atomiques protègent une valeur à la fois. Dès que deux valeurs doivent changer ensemble (un solde et un nombre de transactions, une map et sa taille), utilisez un mutex. Deux opérations atomiques séparées peuvent s'entrelacer avec d'autres goroutines entre elles.
sync.Once
sync.Once exécute une fonction exactement une fois, quel que soit le nombre de goroutines qui l'appellent en même temps. Tous ceux qui appellent Do attendent que le premier appel soit terminé. C'est la façon standard d'initialiser quelque chose paresseusement :
Chaque ligne « runs once » apparaît exactement une fois. Si la fonction passée à Do provoque un panic, Once la considère quand même comme exécutée et ne réessaie jamais. sync.OnceValues fait la même chose pour des fonctions qui renvoient deux valeurs, typiquement une valeur et une erreur.
Mutex, channel ou sync.Map
| Situation | À utiliser |
|---|---|
| Une struct ou une map que plusieurs goroutines modifient sur place | un sync.Mutex dans la struct |
| Surtout des lectures, des écritures occasionnelles, les lectures font un vrai travail | sync.RWMutex |
| Un seul compteur ou flag | sync/atomic |
| Une initialisation unique | sync.Once, sync.OnceValue |
| Transmettre des données d'une goroutine à une autre | un channel |
| Un cache dont les clés sont écrites une fois et lues souvent, ou des goroutines qui touchent des clés disjointes | sync.Map |
sync.Map n'est pas un remplacement général d'une map verrouillée. Elle n'a pas de paramètres de type, donc les valeurs ressortent en any, et elle n'est plus rapide que dans les deux cas du tableau. Commencez par un mutex et une map ordinaire.
Erreurs courantes
- Copier un mutex. Passer par valeur une struct qui contient un
sync.Mutex, ou utiliser un receveur valeur, copie le verrou.go vetsignalepasses lock by valueoucopies lock value. - Verrouiller deux fois dans une goroutine. Les mutex Go ne sont pas réentrants. Si
IncappelleGetet que les deux prennent le verrou,Incbloque indéfiniment. Faites verrouiller les méthodes publiques, et laissez les fonctions privées supposer que le verrou est détenu. - Oublier de déverrouiller lors d'un retour anticipé. Utilisez
defer, sauf si vous avez une raison de ne pas le faire. - Verrouiller dans des ordres différents. Si une goroutine prend le verrou A puis B pendant qu'une autre prend B puis A, les deux peuvent s'attendre indéfiniment. Acquérez toujours plusieurs verrous dans le même ordre.
- Exposer les données protégées. Renvoyer la map interne depuis une méthode permet aux appelants de la lire et de l'écrire sans le verrou. Renvoyez une copie (
maps.Clone, Go 1.21) ou une valeur unique. - Ne protéger que les écritures. Des lectures non verrouillées concurrentes à des écritures verrouillées restent des races. Lancez vos tests avec
go test -race.
Questions fréquentes
Qu'est-ce qu'un mutex en Go ?
Un sync.Mutex est un verrou qui ne laisse qu'une goroutine à la fois exécuter le code entre mu.Lock() et mu.Unlock(). Vous l'utilisez pour protéger des données que plusieurs goroutines lisent et écrivent, comme une map ou une struct. Sa valeur zéro est un mutex déverrouillé, prêt à l'emploi.
Quand utiliser RWMutex plutôt que Mutex ?
Quand les lectures sont bien plus nombreuses que les écritures et que chaque lecture garde le verrou un temps significatif. RLock laisse entrer un nombre quelconque de lecteurs à la fois, tandis que Lock attend un accès exclusif. Pour de courtes sections critiques, un simple Mutex est souvent aussi rapide ou plus, donc mesurez avant de changer.
Une map Go est-elle sûre en accès concurrent ?
Non. Des lectures concurrentes ne posent pas de problème, mais une écriture concurrente à une autre lecture ou écriture est une data race, et le runtime la détecte généralement et plante avec fatal error: concurrent map writes (ou concurrent map read and map write). Protégez la map avec un sync.Mutex ou un sync.RWMutex, ou utilisez sync.Map dans ses cas d'usage précis.
sync.Mutex est-il réentrant en Go ?
Non. Si une goroutine qui détient le verrou appelle de nouveau Lock, elle bloque indéfiniment en s'attendant elle-même. Structurez le code pour que les méthodes exportées prennent le verrou et appellent des fonctions non exportées qui supposent qu'il est déjà détenu.