title: Application Scenarios of sync.Cond in Go (with Examples and Comparisons)
date: 2021-12-04 12:22:19
tags: ['go']#
Recently, while studying "High Performance Go Programming" (https://geektutu.com/post/high-performance-go.html), I came across the concept of sync.Cond
condition variables in Go, and I found it a bit difficult to understand. After researching, I gained some understanding of it and decided to document it.
Concept#
The sync
package provides the condition variable type sync.Cond
, which can be used in combination with a mutex or a read-write lock to coordinate goroutines that want to access shared variables. Its main mechanism is to notify other goroutines that are blocked due to changes in the corresponding shared resource state.
Definition#
sync.Cond
is a struct, defined as follows:
// Each Cond has an associated Locker L (often a *Mutex or *RWMutex),
// which must be held when changing the condition and
// when calling the Wait method.
//
// A Cond must not be copied after first use.
type Cond struct {
noCopy noCopy
// L is held while observing or changing the condition
L Locker
notify notifyList
checker copyChecker
}
This type can be created using the func NewCond(l Locker) *Cond
method in the sync
package.
It has the following three methods:
-
func (c *Cond) Broadcast()
Wakes up all goroutines waiting for
c
. -
func (c *Cond) Signal()
Wakes up a single goroutine waiting for
c
(if any). -
func (c *Cond) Wait()
Automatically unlocks
c.L
and suspends the execution of the goroutine until it is awakened. This function re-locksc.L
before returning.
Application Scenarios#
Based on the above description, the main usage of this structure should be when multiple goroutines call Wait
to wait for a goroutine to run, and the waiting goroutines are awakened by calling Broadcast
or Signal
. In other words, it is mainly used in scenarios where multiple goroutines are waiting and one goroutine notifies the others.
Here's a small example from daily life:
Several students arrive at the cafeteria, but the cafeteria hasn't finished cooking yet, so the students need to wait until the cafeteria finishes cooking before they can eat.
Using the sync.Cond
to describe the above scenario, the following code can be written:
package main
import (
"fmt"
"sync"
"time"
)
var (
ok = false // Whether the cooking is done
food = "" // Name of the food
rwMutex = &sync.RWMutex{} // Read-write lock used to lock the modification of the food name
cond = sync.NewCond(rwMutex.RLocker()) // Condition variable using the read lock of the read-write lock
)
func makeFood() {
// Use a write lock for cooking (of course, because there is only one cooking goroutine, this lock has no practical meaning)
rwMutex.Lock()
defer rwMutex.Unlock()
fmt.Println("The cafeteria starts cooking!")
time.Sleep(3 * time.Second)
ok = true
food = "Fish-flavored Shredded Pork"
fmt.Println("The cafeteria has finished cooking!")
cond.Broadcast()
}
func waitToEat() {
cond.L.Lock()
defer cond.L.Unlock()
for !ok {
cond.Wait()
}
fmt.Println("Finally, I can eat. This meal is", food)
}
func main() {
for i := 0; i < 3; i++ {
go waitToEat()
}
go makeFood()
time.Sleep(4 * time.Second)
}
Running this program produces the following result:
The cafeteria starts cooking!
The cafeteria has finished cooking!
Finally, I can eat. This meal is Fish-flavored Shredded Pork
Finally, I can eat. This meal is Fish-flavored Shredded Pork
Finally, I can eat. This meal is Fish-flavored Shredded Pork
Differences from Other Concepts#
channel#
As we all know, channel
is also a concept used for goroutine synchronization in Go, but unlike sync.Cond
, channel
is mostly used for one-to-one communication. If we extend the previous example, the scenario this time would be:
A student arrives at the cafeteria, but the cafeteria hasn't finished cooking yet, so the student needs to wait until the cafeteria finishes cooking before eating.
Using channel
to describe this:
package main
import (
"fmt"
"time"
)
var (
food = ""
ch = make(chan struct{})
)
func makeFood() {
fmt.Println("The cafeteria starts cooking!")
time.Sleep(3 * time.Second)
food = "Fish-flavored Shredded Pork"
fmt.Println("The cafeteria has finished cooking!")
ch <- struct{}{}
}
func waitToEat() {
<-ch
fmt.Println("Finally, I can eat. This meal is", food)
}
func main() {
go waitToEat()
go makeFood()
time.Sleep(4 * time.Second)
}
The result of running this program is:
The cafeteria starts cooking!
The cafeteria has finished cooking!
Finally, I can eat. This meal is Fish-flavored Shredded Pork
sync.WaitGroup#
sync.WaitGroup
is also used for goroutine synchronization, but its application scenario is the opposite of sync.Cond
. The latter is mostly used for multiple goroutines waiting and one goroutine notifying, while the former is mostly used for one goroutine waiting for multiple goroutines to finish.
Using the same example:
A student arrives at the cafeteria, but none of the cafeteria windows have finished cooking yet. The student wants to wait until each window finishes cooking and then order a portion of food from each window. (This is a bit far-fetched XD)
The code can be described as follows:
package main
import (
"fmt"
"sync"
"time"
)
var (
wg sync.WaitGroup
)
func makeFood() {
fmt.Println("The window starts cooking!")
time.Sleep(3 * time.Second)
fmt.Println("The window has finished cooking!")
wg.Done()
}
func waitToEat() {
wg.Wait()
fmt.Println("Oh, one portion from each window!")
}
func main() {
for i := 0; i < 3; i++ {
wg.Add(1)
go makeFood()
}
go waitToEat()
time.Sleep(4 * time.Second)
}
The result of running this program is:
The window starts cooking!
The window starts cooking!
The window starts cooking!
The window has finished cooking!
The window has finished cooking!
The window has finished cooking!
Oh, one portion from each window!
Conclusion#
The above is my shallow understanding. If there are any mistakes, please feel free to point them out in the comments. Thank you!