Maison > développement back-end > Golang > Allez sync.Cond, le mécanisme de synchronisation le plus négligé

Allez sync.Cond, le mécanisme de synchronisation le plus négligé

DDD
Libérer: 2024-10-30 06:43:28
original
324 Les gens l'ont consulté

Ceci est un extrait du post ; l'article complet est disponible ici : https://victoriametrics.com/blog/go-sync-cond/

Cet article fait partie d'une série sur la gestion de la concurrence dans Go :

  • Allez sync.Mutex : modes normal et famine
  • Allez sync.WaitGroup et le problème d'alignement
  • Allez sync.Pool et les mécanismes derrière cela
  • Go sync.Cond, le mécanisme de synchronisation le plus négligé (nous sommes là)
  • Go sync.Map : le bon outil pour le bon travail
  • Go Singleflight fond dans votre code, pas dans votre base de données

Dans Go, sync.Cond est une primitive de synchronisation, bien qu'elle ne soit pas aussi couramment utilisée que ses frères et sœurs comme sync.Mutex ou sync.WaitGroup. Vous le verrez rarement dans la plupart des projets ou même dans les bibliothèques standards, où d'autres mécanismes de synchronisation ont tendance à prendre sa place.

Cela dit, en tant qu'ingénieur Go, vous ne voulez pas vraiment vous retrouver à lire du code qui utilise sync.Cond et ne pas avoir la moindre idée de ce qui se passe, car cela fait partie de la bibliothèque standard, après tout.

Ainsi, cette discussion vous aidera à combler cet écart et, mieux encore, elle vous donnera une idée plus claire de la façon dont cela fonctionne réellement dans la pratique.

Qu’est-ce que sync.Cond ?

Alors, décomposons ce qu'est sync.Cond.

Lorsqu'une goroutine doit attendre que quelque chose de spécifique se produise, comme une modification de données partagées, elle peut "bloquer", ce qui signifie qu'elle met simplement son travail en pause jusqu'à ce qu'elle obtienne le feu vert pour continuer. La façon la plus simple de le faire est d'utiliser une boucle, peut-être même d'ajouter une durée de veille pour éviter que le processeur ne devienne fou en attendant.

Voici à quoi cela pourrait ressembler :

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Maintenant, ce n'est pas vraiment efficace car cette boucle s'exécute toujours en arrière-plan, brûlant les cycles du processeur, même lorsque rien n'a changé.

C'est là qu'intervient sync.Cond, une meilleure façon de permettre aux goroutines de coordonner leur travail. Techniquement, c'est une « variable de condition » si vous venez d'un milieu plus universitaire.

  • Quand une goroutine attend que quelque chose se produise (attend qu'une certaine condition devienne vraie), elle peut appeler Wait().
  • Une autre goroutine, une fois qu'elle sait que la condition peut être remplie, peut appeler Signal() ou Broadcast() pour réveiller la ou les goroutines en attente et leur faire savoir qu'il est temps de passer à autre chose.

Voici l'interface de base fournie par sync.Cond :

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Go sync.Cond, the Most Overlooked Sync Mechanism

Aperçu de sync.Cond

Très bien, regardons un pseudo-exemple rapide. Cette fois, nous avons un thème Pokémon, imaginez que nous attendons un Pokémon spécifique et que nous voulons avertir les autres goroutines lorsqu'il apparaît.

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Dans cet exemple, une goroutine attend l'apparition de Pikachu, tandis qu'une autre (le producteur) sélectionne au hasard un Pokémon dans la liste et signale au consommateur lorsqu'un nouveau apparaît.

Lorsque le producteur envoie le signal, le consommateur se réveille et vérifie si le bon Pokémon est apparu. Si c'est le cas, on attrape le Pokémon, sinon, le consommateur se rendort et attend le suivant.

Le problème est qu'il y a un écart entre le producteur qui envoie le signal et le consommateur qui se réveille réellement. Entre-temps, le Pokémon pourrait changer, car la goroutine du consommateur peut se réveiller après 1 ms (rarement) ou une autre goroutine modifie le pokémon partagé. Donc sync.Cond dit en gros : 'Hé, quelque chose a changé ! Réveillez-vous et vérifiez, mais si vous arrivez trop tard, cela pourrait encore changer.'

Si le consommateur se réveille tard, le Pokémon pourrait s'enfuir et la goroutine se rendormir.

"Euh, je pourrais utiliser un canal pour envoyer le nom ou le signal du Pokémon à l'autre goroutine"

Absolument. En fait, les canaux sont généralement préférés à sync.Cond in Go car ils sont plus simples, plus idiomatiques et familiers à la plupart des développeurs.

Dans le cas ci-dessus, vous pouvez facilement envoyer le nom du Pokémon via un canal, ou simplement utiliser une struct{} vide pour signaler sans envoyer de données. Mais notre problème ne consiste pas seulement à transmettre des messages via des canaux, il s'agit également de gérer un État partagé.

Notre exemple est assez simple, mais si plusieurs goroutines accèdent à la variable Pokémon partagée, regardons ce qui se passe si nous utilisons un canal :

  • Si nous utilisons un canal pour envoyer le nom du Pokémon, nous aurions toujours besoin d'un mutex pour protéger la variable Pokémon partagée.
  • Si on utilise un canal juste pour signaler, un mutex est quand même nécessaire pour gérer l'accès à l'état partagé.
  • Si nous vérifions Pikachu dans le producteur et que nous l'envoyons ensuite via le canal, nous aurions également besoin d'un mutex. En plus de cela, nous violerions le principe de séparation des préoccupations, selon lequel le producteur assume la logique qui appartient réellement au consommateur.

Cela dit, lorsque plusieurs goroutines modifient des données partagées, un mutex est toujours nécessaire pour les protéger. Vous verrez souvent une combinaison de canaux et de mutex dans ces cas pour garantir une synchronisation appropriée et la sécurité des données.

"D'accord, mais qu'en est-il de la diffusion des signaux ?"

Bonne question ! Vous pouvez en effet imiter un signal diffusé à toutes les goroutines en attente utilisant un canal en le fermant simplement (close(ch)). Lorsque vous fermez un canal, toutes les goroutines recevant ce canal sont averties. Mais gardez à l'esprit qu'un canal fermé ne peut pas être réutilisé, une fois fermé, il reste fermé.

Au fait, il a en fait été question de supprimer sync.Cond dans Go 2 : proposition : sync : supprimer le type Cond.

"Alors, à quoi sert sync.Cond, alors ?"

Eh bien, il existe certains scénarios dans lesquels sync.Cond peut être plus approprié que les canaux.

  1. Avec un canal, vous pouvez soit envoyer un signal à une goroutine en envoyant une valeur, soit notifier toutes les goroutines en fermant le canal, mais vous ne pouvez pas faire les deux. sync.Cond vous offre un contrôle plus précis. Vous pouvez appeler Signal() pour réveiller une seule goroutine ou Broadcast() pour toutes les réveiller.
  2. Et vous pouvez appeler Broadcast() autant de fois que nécessaire, ce que les chaînes ne peuvent pas faire une fois fermées (la fermeture d'une chaîne fermée déclenchera une panique).
  3. Les canaux ne fournissent pas de moyen intégré de protection des données partagées : vous devrez les gérer séparément avec un mutex. sync.Cond, en revanche, vous offre une approche plus intégrée en combinant le verrouillage et la signalisation dans un seul package (et de meilleures performances).

"Pourquoi le verrou est-il intégré dans sync.Cond ?"

En théorie, une variable de condition comme sync.Cond n'a pas besoin d'être liée à un verrou pour que sa signalisation fonctionne.

Vous pouvez demander aux utilisateurs de gérer leurs propres verrous en dehors de la variable de condition, ce qui peut sembler donner plus de flexibilité. Ce n'est pas vraiment une limitation technique mais plutôt une erreur humaine.

Le gérer manuellement peut facilement conduire à des erreurs car le schéma n'est pas vraiment intuitif, il faut déverrouiller le mutex avant d'appeler Wait(), puis le verrouiller à nouveau lorsque la goroutine se réveille. Ce processus peut sembler délicat et est assez sujet aux erreurs, comme oublier de verrouiller ou de déverrouiller au bon moment.

Mais pourquoi le motif semble-t-il un peu décalé ?

En général, les goroutines qui appellent cond.Wait() doivent vérifier un état partagé dans une boucle, comme ceci :

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Le verrou intégré dans sync.Cond nous aide à gérer le processus de verrouillage/déverrouillage, rendant le code plus propre et moins sujet aux erreurs, nous discuterons bientôt du modèle en détail.

Comment l'utiliser ?

Si vous regardez attentivement l'exemple précédent, vous remarquerez un modèle cohérent chez consumer : nous verrouillons toujours le mutex avant d'attendre (.Wait()) la condition, et nous le déverrouillons une fois la condition remplie.

De plus, nous enveloppons la condition d'attente dans une boucle, voici un rappel :

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Cond.Attendez()

Lorsque nous appelons Wait() sur un sync.Cond, nous disons à la goroutine actuelle de patienter jusqu'à ce qu'une condition soit remplie.

Voici ce qui se passe dans les coulisses :

  1. La goroutine est ajoutée à une liste d'autres goroutines qui attendent également dans cette même condition. Toutes ces goroutines sont bloquées, ce qui signifie qu'elles ne peuvent pas continuer tant qu'elles ne sont pas « réveillées » par un appel Signal() ou Broadcast().
  2. L'élément clé ici est que le mutex doit être verrouillé avant d'appeler Wait() car Wait() fait quelque chose d'important, il libère automatiquement le verrou (appelle Unlock()) avant de mettre la goroutine en veille. Cela permet à d'autres goroutines de saisir le verrou et de faire leur travail pendant que la goroutine d'origine attend.
  3. Lorsque la goroutine en attente est réveillée (par Signal() ou Broadcast()), elle ne reprend pas immédiatement son travail. Tout d'abord, il doit réacquérir le verrou (Lock()).

Go sync.Cond, the Most Overlooked Sync Mechanism

La méthode sync.Cond.Wait()

Voici un aperçu du fonctionnement de Wait() sous le capot :

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Même si c'est simple, on peut en retenir 4 points principaux :

  1. Il y a un vérificateur pour empêcher la copie de l'instance Cond, ce serait la panique si vous le faisiez.
  2. Appeler cond.Wait() déverrouille immédiatement le mutex, donc le mutex doit être verrouillé avant d'appeler cond.Wait(), sinon cela paniquera.
  3. Après avoir été réveillé, cond.Wait() reverrouille le mutex, ce qui signifie que vous devrez le déverrouiller à nouveau une fois que vous aurez terminé avec les données partagées.
  4. La plupart des fonctionnalités de sync.Cond sont implémentées dans le runtime Go avec une structure de données interne appelée notifyList, qui utilise un système basé sur des tickets pour les notifications.

En raison de ce comportement de verrouillage/déverrouillage, vous suivrez un modèle typique lorsque vous utiliserez sync.Cond.Wait() pour éviter les erreurs courantes :

// Suspends the calling goroutine until the condition is met
func (c *Cond) Wait() {}

// Wakes up one waiting goroutine, if there is one
func (c *Cond) Signal() {}

// Wakes up all waiting goroutines
func (c *Cond) Broadcast() {}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Go sync.Cond, the Most Overlooked Sync Mechanism

Le modèle typique d'utilisation de sync.Cond.Wait()

"Pourquoi ne pas simplement utiliser c.Wait() directement sans boucle ?"


Ceci est un extrait du post ; l'article complet est disponible ici : https://victoriametrics.com/blog/go-sync-cond/

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal