paint-brush
sync.Cond in Go verstehen: Ein Leitfaden für Anfängerby@ivanlemeshev
1,908
1,908

sync.Cond in Go verstehen: Ein Leitfaden für Anfänger

Ivan Lemeshev9m2024/04/28
Read on Terminal Reader

Zusammenfassend lässt sich sagen, dass sync.Cond ein nützlicher Typ in der Programmiersprache Go ist, der die Synchronisierung und Koordination zwischen Goroutinen basierend auf bestimmten Bedingungen ermöglicht. Er bietet eine Möglichkeit, Bedingungsvariablen zu erstellen und zu verwalten. Er verfügt über Methoden zum Warten auf, Signalisieren und Senden von Bedingungen. Durch die Verwendung von `sync.Cond` können Sie kontrolliertere und synchronisiertere parallele Programme in Go schreiben. Es ist wichtig zu beachten, dass sync.Cond nur eines der von der Go-Standardbibliothek bereitgestellten Synchronisierungsprimitive ist und seine Verwendung von den spezifischen Anforderungen Ihres parallelen Programms abhängt. In einigen Fällen sind andere Synchronisierungsprimitive wie Kanäle oder sync.WaitGroup möglicherweise besser geeignet.
featured image - sync.Cond in Go verstehen: Ein Leitfaden für Anfänger
Ivan Lemeshev HackerNoon profile picture
0-item

Einführung

Ich möchte den Typ und die Anwendungsfälle von sync.Cond sowie dessen Einsatzzweck besprechen.

Was ist sync.Cond ?

In der Programmiersprache Go ist sync.Cond ein im sync Paket definierter Typ, der eine Bedingungsvariable darstellt. Bedingungsvariablen sind Synchronisierungsprimitive, die zur Koordination von Goroutinen verwendet werden, indem sie es ihnen ermöglichen, zu warten, bis eine bestimmte Bedingung erfüllt ist, bevor sie fortfahren.


Der Typ sync.Cond bietet eine Möglichkeit zum Erstellen und Verwalten von Bedingungsvariablen. Er verfügt über drei Hauptmethoden:


  1. Wait() : Diese Methode bewirkt, dass die aufrufende Goroutine wartet, bis eine andere Goroutine die Bedingungsvariable signalisiert. Wenn die Goroutine Wait() aufruft, gibt sie die zugehörige Sperre frei und unterbricht die Ausführung, bis eine andere Goroutine Signal() oder Broadcast() für dieselbe Variable sync.Cond aufruft.


  2. Signal() : Diese Methode weckt eine Goroutine, die auf die Bedingungsvariable wartet. Wenn mehrere Goroutinen warten, wird nur eine davon geweckt. Die Wahl, welche Goroutine geweckt wird, ist willkürlich und nicht garantiert.


  3. Broadcast() : Diese Methode weckt alle Goroutinen, die auf die Bedingungsvariable warten. Wenn Broadcast() aufgerufen wird, werden alle wartenden Goroutinen geweckt und können fortfahren.


Beachten Sie, dass sync.Cond ein zugehöriges sync.Mutex erfordert, um den Zugriff auf die Bedingungsvariable zu synchronisieren.


Durch die Verwendung von sync.Cond können Sie die Ausführung von Goroutinen basierend auf bestimmten Bedingungen koordinieren und so eine kontrolliertere und synchronisiertere gleichzeitige Programmierung in Go ermöglichen.

Häufige Anwendungsfälle

sync.Cond wird häufig in Szenarien verwendet, in denen Goroutinen basierend auf bestimmten Bedingungen koordiniert werden und miteinander kommunizieren müssen. Betrachten wir gängige Anwendungsfälle für sync.Cond .

Goroutine-Synchronisierung

sync.Cond kann verwendet werden, um die Ausführung mehrerer Goroutinen zu synchronisieren. Sie haben beispielsweise mehrere Goroutinen, die warten müssen, bis eine bestimmte Bedingung erfüllt ist, bevor sie fortfahren können. Die wartenden Goroutinen können cond.Wait() aufrufen, und die signalisierende Goroutine kann cond.Signal() oder cond.Broadcast() aufrufen, um die wartenden Goroutinen aufzuwecken, wenn die Bedingung erfüllt ist.


 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() }


In diesem Beispiel haben wir zwei Goroutinen. Die erste Goroutine wartet mit cond.Wait() auf eine Bedingung, während die zweite Goroutine die Bedingung mit cond.Signal() signalisiert.


Wenn das Programm ausgeführt wird, erhält die erste Goroutine die Sperre und ruft dann cond.Wait() auf. Da die Bedingung noch nicht erfüllt ist, gibt die erste Goroutine die Sperre frei und unterbricht ihre Ausführung.


In der Zwischenzeit schläft die zweite Goroutine fünf Sekunden lang und simuliert einige Arbeiten. Sie erhält die Sperre und ruft dann cond.Signal() auf. Sie weckt die wartende Goroutine, die dann die Sperre erhält und ausgeführt wird.


Die Verwendung von sync.Cond stellt sicher, dass die erste Goroutine wartet, bis die zweite Goroutine die Bedingung signalisiert, wodurch eine Synchronisierung und Koordination zwischen den beiden Goroutinen ermöglicht wird.

Produzent-Konsumenten-Problem

sync.Cond kann bei der Lösung des Erzeuger-Verbraucher-Problems nützlich sein, einem klassischen Synchronisationsproblem, bei dem zwei Arten von Prozessen beteiligt sind, Erzeuger und Verbraucher, die sich einen gemeinsamen Puffer oder eine Warteschlange mit fester Größe teilen. Die Erzeuger-Goroutinen können cond.Signal() oder cond.Broadcast() verwenden, um die Verbraucher-Goroutinen zu benachrichtigen, wenn neue Daten zum Verbrauch verfügbar sind.


 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() }


In diesem Beispiel haben wir eine Produzenten-Goroutine, die Nachrichten produziert und sie dem Nachrichtenkanal hinzufügt, und eine Konsumenten-Goroutine, die Nachrichten konsumiert. Der Nachrichtenkanal hat eine maximale Größe, die durch MaxMessageChannelSize definiert ist.


Die Produzenten-Goroutine fügt Nachrichten zum Nachrichtenkanal hinzu und verwendet cond.Signal() , um die Konsumenten-Goroutine zu benachrichtigen, wenn neue Daten verfügbar sind. Wenn der Nachrichtenkanal voll ist, wartet die Produzenten-Goroutine mit cond.Wait() , bis der Konsument einige Daten verbraucht und Platz im Nachrichtenkanal freigibt.


In ähnlicher Weise konsumiert die Consumer-Goroutine Nachrichten aus dem Nachrichtenkanal und verwendet cond.Signal() um die Producer-Goroutine zu benachrichtigen, wenn im Nachrichtenkanal Platz frei wird. Wenn dieser leer ist, wartet die Consumer-Goroutine mit cond.Wait() , bis der Producer Daten produziert und sie dem Nachrichtenkanal hinzufügt.


Hier ermöglicht sync.Cond die Koordination und Synchronisierung zwischen den Goroutinen des Produzenten und Konsumenten. Es stellt sicher, dass der Konsument wartet, wenn der Nachrichtenkanal leer ist, und der Produzent wartet, wenn er voll ist, wodurch das Produzenten-Konsumenten-Problem gelöst wird.

Ressourcensynchronisierung

Angenommen, mehrere Goroutinen benötigen exklusiven Zugriff auf eine gemeinsam genutzte Ressource. sync.Cond kann verwendet werden, um den Zugriff zu koordinieren. Beispielsweise muss ein Pool von Worker-Goroutinen möglicherweise warten, bis eine bestimmte Anzahl von Ressourcen verfügbar ist, bevor sie mit der Verarbeitung beginnen können. Die Goroutinen können mit cond.Wait() auf die Bedingungsvariable warten und mit cond.Signal() oder cond.Broadcast() über die Freigabe von Ressourcen benachrichtigen.


 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() }


In diesem Beispiel haben wir mehrere Worker-Goroutinen, die exklusiven Zugriff auf begrenzte Ressourcen benötigen. Die Worker-Goroutinen beschaffen und geben Ressourcen frei, indem sie cond.Signal() verwenden, um sich mit anderen Workern abzustimmen. Wenn keine Ressourcen verfügbar sind, warten die Worker-Goroutinen using cond.Wait() , bis die andere Goroutine die Ressource freigibt.


In diesem Beispiel ermöglicht sync.Cond die Synchronisierung und Koordination zwischen den Worker-Goroutinen und stellt sicher, dass die Worker-Goroutinen warten, wenn keine Ressourcen verfügbar sind, wodurch der Ressourcenzugriff effektiv synchronisiert wird.

Ereignisbenachrichtigung

sync.Cond kann verwendet werden, um Goroutinen über bestimmte Ereignisse oder Änderungen im System zu benachrichtigen. Sie können beispielsweise Goroutinen auf ein bestimmtes Ereignis warten lassen. Wenn das Ereignis eintritt, kann die signalisierende Goroutine cond.Signal() oder cond.Broadcast() verwenden, um die wartenden Goroutinen aufzuwecken und ihnen zu ermöglichen, das Ereignis zu verarbeiten.


 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() }


Hier haben wir mehrere Worker-Goroutinen, die an einem Barrierepunkt arbeiten und synchronisieren. Die Worker-Goroutinen erhöhen einen Zähler und warten dann entweder an der Barriere oder signalisieren der Barriere mit cond.Wait() und cond.Broadcast() basierend auf der Anzahl der Worker, die die Barriere erreichen.


Jede Worker-Goroutine führt eine Arbeit aus und erhält dann die Sperre, um die Zählervariable zu erhöhen. Wenn der aktuelle Worker als letzter die Barriere erreicht, sendet er den Barrierezustand mit cond.Broadcast() um alle wartenden Worker aufzuwecken. Andernfalls wartet er mit cond.Wait() an der Barriere, um vom letzten Worker benachrichtigt zu werden.


Die Barrierensynchronisierung stellt sicher, dass alle Worker-Goroutinen die Barriere erreichen, bevor eine von ihnen darüber hinausgeht. Dies kann in Szenarien nützlich sein, in denen die Ausführung mehrerer Goroutinen an einem bestimmten Punkt in ihrem Workflow synchronisiert werden muss.


Beachten Sie, dass die Barriere in diesem Beispiel mithilfe eines einfachen Zählers implementiert wird. In komplexeren Szenarien müssen Sie jedoch möglicherweise zusätzliche Synchronisierungsmechanismen oder -bedingungen berücksichtigen, um eine korrekte Synchronisierung sicherzustellen und Race Conditions zu vermeiden.

Abschluss

Zusammenfassend lässt sich sagen, dass sync.Cond ein nützlicher Typ in der Programmiersprache Go ist, der die Synchronisierung und Koordination zwischen Goroutinen basierend auf bestimmten Bedingungen ermöglicht. Er bietet eine Möglichkeit, Bedingungsvariablen zu erstellen und zu verwalten. Er verfügt über Methoden zum Warten auf, Signalisieren und Senden von Bedingungen. Durch die Verwendung von „sync.Cond“ können Sie kontrolliertere und synchronisiertere parallele Programme in Go schreiben.


Es ist wichtig zu beachten, dass sync.Cond nur eines der Synchronisationsprimitive ist, die von der Go-Standardbibliothek bereitgestellt werden, und dass seine Verwendung von den spezifischen Anforderungen Ihres parallelen Programms abhängt. In einigen Fällen sind andere Synchronisationsprimitive wie Kanäle oder sync.WaitGroup möglicherweise besser geeignet.