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 :
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.
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) }
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.
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() {}
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) }
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 :
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.
"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) }
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.
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() {}
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 :
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) }
Même si c'est simple, on peut en retenir 4 points principaux :
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() {}
"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!