paint-brush
Comprendre sync.Cond in Go : un guide pour les débutantspar@ivanlemeshev
7,416 lectures
7,416 lectures

Comprendre sync.Cond in Go : un guide pour les débutants

par Ivan Lemeshev9m2024/04/28
Read on Terminal Reader

Trop long; Pour lire

En conclusion, sync.Cond est un type utile dans le langage de programmation Go qui permet la synchronisation et la coordination entre les goroutines en fonction de conditions spécifiques. Il fournit un moyen de créer et de gérer des variables de condition. Il dispose de méthodes pour attendre, signaler et diffuser les conditions. En utilisant `sync.Cond`, vous pouvez écrire des programmes simultanés plus contrôlés et synchronisés dans Go. Il est important de noter que sync.Cond n'est qu'une des primitives de synchronisation fournies par la bibliothèque standard Go et que son utilisation dépend des exigences spécifiques de votre programme concurrent. Dans certains cas, d'autres primitives de synchronisation comme les canaux ou sync.WaitGroup peuvent être plus adaptées.
featured image - Comprendre sync.Cond in Go : un guide pour les débutants
Ivan Lemeshev HackerNoon profile picture
0-item

Introduction

Je souhaite discuter du type sync.Cond , des cas d'utilisation et du moment de l'utiliser.

Qu'est-ce que sync.Cond ?

Dans le langage de programmation Go, sync.Cond est un type défini dans le package sync représentant une variable de condition. Les variables de condition sont des primitives de synchronisation utilisées pour coordonner les goroutines en leur permettant d'attendre qu'une condition spécifique devienne vraie avant de continuer.


Le type sync.Cond fournit un moyen de créer et de gérer des variables de condition. Il dispose de trois méthodes principales :


  1. Wait() : Cette méthode oblige la goroutine appelante à attendre qu'une autre goroutine signale la variable de condition. Lorsque la goroutine appelle Wait() , elle libère le verrou associé et suspend l'exécution jusqu'à ce qu'une autre goroutine appelle Signal() ou Broadcast() sur la même variable sync.Cond .


  2. Signal() : Cette méthode réveille une goroutine en attente de la variable de condition. Si plusieurs goroutines attendent, une seule d'entre elles est réveillée. Le choix de la goroutine qui sera réveillée est arbitraire et non garanti.


  3. Broadcast() : Cette méthode réveille toutes les goroutines en attente de la variable de condition. Lorsque Broadcast() est appelé, toutes les goroutines en attente sont réveillées et peuvent continuer.


Notez que sync.Cond nécessite un sync.Mutex associé pour synchroniser l'accès à la variable de condition.


En utilisant sync.Cond , vous pouvez coordonner l'exécution de goroutines en fonction de conditions spécifiques, permettant une programmation simultanée plus contrôlée et synchronisée dans Go.

Cas d'utilisation courants

sync.Cond est couramment utilisé dans des scénarios où les goroutines doivent se coordonner et communiquer entre elles en fonction de conditions spécifiques. Considérons les cas d'utilisation courants de sync.Cond .

Synchronisation Goroutine

sync.Cond peut être utilisé pour synchroniser l'exécution de plusieurs goroutines. Par exemple, vous pouvez avoir plusieurs goroutines qui doivent attendre qu'une condition spécifique soit remplie avant de continuer. Les goroutines en attente peuvent appeler cond.Wait() , et la goroutine de signalisation peut appeler cond.Signal() ou cond.Broadcast() pour réveiller les goroutines en attente lorsque la condition est remplie.


 package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) wg.Add(2) go func() { fmt.Println("Goroutine 1 is started") defer wg.Done() cond.L.Lock() defer cond.L.Unlock() fmt.Println("Goroutine 1 is waiting for condition") cond.Wait() fmt.Println("Goroutine 1 met the condition") fmt.Println("Goroutine 1 is done") }() go func() { fmt.Println("Goroutine 2 is started") defer wg.Done() time.Sleep(5 * time.Second) // Simulating some work cond.L.Lock() defer cond.L.Unlock() fmt.Println("Goroutine 2 is signaling condition") cond.Signal() fmt.Println("Goroutine 2 completed signaling") fmt.Println("Goroutine 2 is done") }() wg.Wait() }


Dans cet exemple, nous avons deux goroutines. La première goroutine attend une condition en utilisant cond.Wait() , tandis que la seconde goroutine signale la condition en utilisant cond.Signal() .


Lorsque le programme s'exécute, la première goroutine acquiert le verrou puis appelle cond.Wait() . La condition n’étant pas encore remplie, la première goroutine libère le verrou et suspend son exécution.


Pendant ce temps, la deuxième goroutine dort pendant cinq secondes, simulant un travail. Il acquiert le verrou puis appelle cond.Signal() . Il réveille la goroutine en attente, qui acquiert ensuite le verrou et s'exécute.


L'utilisation de sync.Cond garantit que la première goroutine attend que la seconde goroutine signale la condition, permettant ainsi la synchronisation et la coordination entre les deux goroutines.

Problème producteur-consommateur

sync.Cond peut être utile pour résoudre le problème producteur-consommateur , un problème de synchronisation classique impliquant deux types de processus, producteurs et consommateurs, qui partagent un tampon ou une file d'attente commune de taille fixe. Les goroutines productrices peuvent utiliser cond.Signal() ou cond.Broadcast() pour avertir les goroutines consommatrices lorsque de nouvelles données sont disponibles pour la consommation.


 package main import ( "fmt" "sync" "time" ) const MaxMessageChannelSize = 5 func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) messageChannel := NewMessageChannel(MaxMessageChannelSize) producer := NewProducer(cond, messageChannel) consumer := NewConsumer(cond, messageChannel) wg.Add(2) go func() { defer wg.Done() for i := range 10 { producer.Produce(fmt.Sprintf("Message %d", i)) } }() go func() { defer wg.Done() for range 10 { consumer.Consume() } }() wg.Wait() } type MessageChannel struct { maxBufferSize int buffer []string } func NewMessageChannel(size int) *MessageChannel { return &MessageChannel{ maxBufferSize: size, buffer: make([]string, 0, size), } } func (mc *MessageChannel) IsEmpty() bool { return len(mc.buffer) == 0 } func (mc *MessageChannel) IsFull() bool { return len(mc.buffer) == mc.maxBufferSize } func (mc *MessageChannel) Add(message string) { mc.buffer = append(mc.buffer, message) } func (mc *MessageChannel) Get() string { message := mc.buffer[0] mc.buffer = mc.buffer[1:] return message } type Producer struct { cond *sync.Cond messageChannel *MessageChannel } func NewProducer(cond *sync.Cond, messageChannel *MessageChannel) *Producer { return &Producer{ cond: cond, messageChannel: messageChannel, } } func (p *Producer) Produce(message string) { time.Sleep(500 * time.Millisecond) // Simulating some work p.cond.L.Lock() defer p.cond.L.Unlock() for p.messageChannel.IsFull() { fmt.Println("Producer is waiting because the message channel is full") p.cond.Wait() } p.messageChannel.Add(message) fmt.Println("Producer produced the message:", message) p.cond.Signal() } type Consumer struct { id int cond *sync.Cond messageChannel *MessageChannel } func NewConsumer(cond *sync.Cond, messageChannel *MessageChannel) *Consumer { return &Consumer{ cond: cond, messageChannel: messageChannel, } } func (c *Consumer) Consume() { time.Sleep(1 * time.Second) // Simulating some work c.cond.L.Lock() defer c.cond.L.Unlock() for c.messageChannel.IsEmpty() { fmt.Println("Consumer is waiting because the message channel is empty") c.cond.Wait() } message := c.messageChannel.Get() fmt.Println("Consumer consumed the message:", message) c.cond.Signal() }


Dans cet exemple, nous avons un goroutine producteur qui produit des messages et les ajoute au canal de messages et un goroutine consommateur qui consomme des messages. Le canal de message a une taille maximale définie par MaxMessageChannelSize .


La goroutine productrice ajoute des messages au canal de messages et utilise cond.Signal() pour avertir la goroutine consommatrice lorsque de nouvelles données sont disponibles. Si le canal de message est plein, la goroutine du producteur attend en utilisant cond.Wait() jusqu'à ce que le consommateur consomme des données et libère de l'espace dans le canal de message.


De même, la goroutine consommatrice consomme les messages du canal de messages et utilise cond.Signal() pour avertir la goroutine productrice lorsque de l'espace devient disponible dans le canal de messages. S'il est vide, la goroutine du consommateur attend en utilisant cond.Wait() jusqu'à ce que le producteur produise des données et les ajoute au canal de message.


Ici, sync.Cond permet la coordination et la synchronisation entre les goroutines producteur et consommateur. Cela garantit que le consommateur attend lorsque le canal de message est vide et que le producteur attend lorsqu'il est plein, résolvant ainsi le problème producteur-consommateur.

Synchronisation des ressources

Supposons que plusieurs goroutines aient besoin d'un accès exclusif à une ressource partagée. sync.Cond peut être utilisé pour coordonner l’accès. Par exemple, un pool de goroutines de travail peut devoir attendre qu'un certain nombre de ressources soient disponibles avant de pouvoir commencer le traitement. Les goroutines peuvent attendre la variable de condition en utilisant cond.Wait() et notifier la libération de la ressource en utilisant cond.Signal() ou cond.Broadcast() .


 package main import ( "fmt" "sync" "time" ) const MaxResources = 3 func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) resourceProvider := NewResourceProvider(cond, MaxResources) wg.Add(10) for i := range 10 { go func(workerID int) { defer wg.Done() worker := NewWorker(workerID, cond, resourceProvider) worker.Run() }(i) } wg.Wait() } type ResourceProvider struct { maxResources int availableResources int cond *sync.Cond } func NewResourceProvider(cond *sync.Cond, maxResources int) *ResourceProvider { return &ResourceProvider{ cond: cond, availableResources: maxResources, } } func (rp *ResourceProvider) AvailableResources() int { return rp.availableResources } func (rp *ResourceProvider) AcquireResoirce() { rp.availableResources-- } func (rp *ResourceProvider) ReleaseResource() { rp.availableResources++ } type Worker struct { id int cond *sync.Cond rp *ResourceProvider } func NewWorker(workerID int, cond *sync.Cond, rp *ResourceProvider) *Worker { return &Worker{ id: workerID, cond: cond, rp: rp, } } func (w *Worker) Run() { w.cond.L.Lock() for w.rp.AvailableResources() == 0 { fmt.Printf("Worker %d is waiting for resources\n", w.id) w.cond.Wait() } w.rp.AcquireResoirce() fmt.Printf("Worker %d acquired resource. Remaining resources: %d\n", w.id, w.rp.AvailableResources()) w.cond.L.Unlock() time.Sleep(1 * time.Second) // Simulating work w.cond.L.Lock() defer w.cond.L.Unlock() w.rp.ReleaseResource() fmt.Printf("Worker %d released resource. Remaining resources: %d\n", w.id, w.rp.AvailableResources()) w.cond.Signal() }


Dans cet exemple, nous avons plusieurs goroutines de travail qui ont besoin d'un accès exclusif à des ressources limitées. Les goroutines de travail acquièrent et libèrent des ressources en utilisant cond.Signal() pour se coordonner avec d'autres travailleurs. Si aucune ressource n'est disponible, les goroutines de travail attendent using cond.Wait() jusqu'à ce que l'autre goroutine libère la ressource.


Dans cet exemple, sync.Cond permet la synchronisation et la coordination entre les goroutines de travail, garantissant que les goroutines de travail attendent lorsqu'aucune ressource n'est disponible, synchronisant ainsi efficacement l'accès aux ressources.

Notification d'événement

sync.Cond peut être utilisé pour informer les goroutines d'événements ou de changements spécifiques dans le système. Par exemple, vous pouvez avoir des goroutines attendant un événement spécifique. Lorsque l'événement se produit, la goroutine de signalisation peut utiliser cond.Signal() ou cond.Broadcast() pour réveiller les goroutines en attente et leur permettre de gérer l'événement.


 package main import ( "fmt" "sync" "time" ) const maxWorkersCount = 10 func main() { var counter int32 var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) wg.Add(maxWorkersCount) for i := range maxWorkersCount { go func(workerID int) { defer wg.Done() fmt.Printf("Worker %d performing work\n", workerID) time.Sleep(1 * time.Second) // Simulate work cond.L.Lock() defer cond.L.Unlock() counter++ if counter == maxWorkersCount { fmt.Println("All workers have reached the barrier") cond.Broadcast() } else { fmt.Printf("Worker %d is waiting at the barrier\n", workerID) cond.Wait() } fmt.Printf("Worker %d passed the barrier\n", workerID) }(i) } wg.Wait() }


Ici, nous avons plusieurs goroutines de travail qui effectuent un travail et se synchronisent à un point de barrière. Les goroutines de travailleurs incrémentent un compteur, puis attendent à la barrière ou signalent la barrière à l'aide cond.Wait() et cond.Broadcast() en fonction du nombre de travailleurs atteignant la barrière.


Chaque goroutine de travail effectue un travail puis acquiert le verrou pour incrémenter la variable du compteur. Si le travailleur actuel est le dernier à atteindre la barrière, il diffuse la condition de la barrière à l'aide de cond.Broadcast() pour réveiller tous les travailleurs en attente. Sinon, il attend à la barrière en utilisant cond.Wait() pour être averti par le dernier travailleur.


La synchronisation des barrières garantit que tous les goroutines de travailleurs atteignent la barrière avant qu'aucun d'entre eux ne la dépasse. Cela peut être utile dans les scénarios nécessitant de synchroniser l'exécution de plusieurs goroutines à un point spécifique de leur flux de travail.


Notez que la barrière est implémentée à l’aide d’un simple compteur dans cet exemple. Toutefois, dans des scénarios plus complexes, vous devrez peut-être envisager des mécanismes ou des conditions de synchronisation supplémentaires pour garantir une synchronisation correcte et éviter les conditions de concurrence.

Conclusion

En conclusion, sync.Cond est un type utile dans le langage de programmation Go qui permet la synchronisation et la coordination entre les goroutines en fonction de conditions spécifiques. Il fournit un moyen de créer et de gérer des variables de condition. Il dispose de méthodes pour attendre, signaler et diffuser les conditions. En utilisant `sync.Cond`, vous pouvez écrire des programmes simultanés plus contrôlés et synchronisés dans Go.


Il est important de noter que sync.Cond n'est qu'une des primitives de synchronisation fournies par la bibliothèque standard Go et que son utilisation dépend des exigences spécifiques de votre programme concurrent. Dans certains cas, d'autres primitives de synchronisation comme les canaux ou sync.WaitGroup peuvent être plus adaptées.