Menu

Mutex en Golang : sync.Mutex, RWMutex, atomic et sync.Once

Comment protéger un état partagé entre goroutines avec sync.Mutex et sync.RWMutex, quand sync/atomic suffit, comment sync.Once exécute une initialisation exactement une fois, et les erreurs de verrouillage qui causent des deadlocks.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

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.Mutex est 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ès Lock garantit 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 placeun sync.Mutex dans la struct
Surtout des lectures, des écritures occasionnelles, les lectures font un vrai travailsync.RWMutex
Un seul compteur ou flagsync/atomic
Une initialisation uniquesync.Once, sync.OnceValue
Transmettre des données d'une goroutine à une autreun channel
Un cache dont les clés sont écrites une fois et lues souvent, ou des goroutines qui touchent des clés disjointessync.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 vet signale passes lock by value ou copies lock value.
  • Verrouiller deux fois dans une goroutine. Les mutex Go ne sont pas réentrants. Si Inc appelle Get et que les deux prennent le verrou, Inc bloque 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.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER