Published
- 5 min read
sync.Cond
Background
When we have multiple tasks to do, some tasks may have dependence on other tasks, in other words, there could be some order we need to adhere to.
sync.Cond
provides a mechanism for us to coordinate goroutines and achieve certain order.
ELI5 Why we need it
Imagine in life we have certain milestones:
- Getting our first job.
- Buying a house.
- Getting Married
- Retirement
- etc
Milestones like getting a job, or getting married could happen anytime, but milestones like retirement could require us to get married and own a house first.
Usually, people would reevaluate their life situation every once in a while. Perhaps at the start of each year to see if they completed a milestone and are ready to work towards another milestone.
Having sync.Cond
is like having a personal manager. Every time you have completed a certain milestone(condition) your personal manager will ask you to reevaluate your life immediately and you can move on ASAP.
Hence, instead of waiting till the start of every year, your change of goals becomes near real-time.
How to use it
First, we define our milestones as an Event and a thread-safe Tracker:
type Event struct {
Name EventName // event identifier
Start func() // function to execute
Prerequisites []EventName // prerequisites before we can start this event
}
type Tracker struct {
// For simplicity, we will use 2 maps here to track statuses
// In actual world we will probably have more statuses and we need enum
completedEvents map[EventName]bool
startedEvents map[EventName]bool
mu sync.RWMutex
}
// There are some helper functions for Tracker that I'll omit here. If you're interested, feel free to scroll to the bottom
Next, we define our milestones along with their prerequisites:
var events = []Event{
{
Name: BuyHouse,
Start: func() {
time.Sleep(1*time.Second) // mimic taking time to complete the milestone
fmt.Println("BoughtHouse!")
},
Prerequisites: []EventName{GetJob},
},
{
Name: GetJob,
Start: func() {
time.Sleep(1*time.Second)
fmt.Println("SecuredJob!")
},
Prerequisites: []EventName{CompleteDegree},
},
{
Name: CompleteDegree,
Start: func() {
time.Sleep(1*time.Second)
fmt.Println("CompleteDegree!")
},
Prerequisites: []EventName{},
},
{
Name: GetMarried,
Start: func() {
time.Sleep(1*time.Second)
fmt.Println("IAmMarried!")
},
Prerequisites: []EventName{},
},
{
Name: Retirement,
Start: func() {
time.Sleep(1*time.Second)
fmt.Println("FIRE!!")
},
Prerequisites: []EventName{GetMarried, BuyHouse},
},
}
Now, let us see what happens when we live with a fixed schedule instead of a personal manager. We mimic this by polling every second to check if we can start our next milestone.
func liveLifeWithScheduler() {
var (
startTs = time.Now()
wg = errgroup.Group{}
tracker = &Tracker{
completedEvents: make(map[EventName]bool),
startedEvents: make(map[EventName]bool),
}
)
for tracker.GetNumStartedEvent() < len(events) {
// polling every second to check which event is ready
for _, event := range events {
// event has started or prerequisites not completed; we can ignore
if tracker.hasEventStarted(event.Name) ||
!tracker.hasCompletedPrerequisites(event.Prerequisites) {
continue
}
e := event // create copy for goroutine
wg.Go(func() error {
e.Start() // start event
tracker.completeEvent(e.Name) // update event status in tracker
return nil
})
tracker.startEvent(event.Name) // update event status in tracker
}
time.Sleep(1*time.Second)
}
wg.Wait()
fmt.Println(time.Since(startTs).Seconds())
}
// Output
// CompleteDegree!
// IAmMarried!
// SecuredJob!
// BoughtHouse!
// FIRE!!
// 6.007259125s <- time taken
VS with sync.Cond
:
func liveLifeWithCond() {
var (
startTs = time.Now()
cond = sync.NewCond(&sync.Mutex{})
wg = errgroup.Group{}
tracker = &Tracker{
completedEvents: make(map[EventName]bool),
}
)
for _, e := range events {
event := e
wg.Go(func() error {
cond.L.Lock() // obtain lock
// blocked until prerequisites is done
for !tracker.hasCompletedPrerequisites(event.Prerequisites) {
cond.Wait() // releases lock and wait for signal/broadcast by other goroutines
}
cond.L.Unlock() // release lock for other goroutines to run concurrently
event.Start() // start event
tracker.completeEvent(event.Name) // update event status in tracker
cond.Broadcast() // wake up other goroutines sleeping in cond.Wait()
return nil
})
}
wg.Wait()
fmt.Println(time.Since(startTs).Seconds())
}
// Output
// CompleteDegree!
// IAmMarried!
// SecuredJob!
// BoughtHouse!
// FIRE!!
// 4.004059958s <- time taken
Awesome, less time is wasted on polling and events get to start as soon as prerequisite events are completed.
Rabbit Hole
In reality, there probably aren’t many scenarios where sync.Cond
would be a perfect fit.
In the official Golang documents, it is stated that most use cases are better off using channels.
I do agree because I took some time to squeeze out this contrived scenario above. It is somewhat related to what I needed to implement at work, albeit we didn’t rely on sync.Cond
.
Based on what I have read online so far, sync.Cond
doesn’t have a lot of love in the community. But hey, I believe my above scenario fits pretty well.
If you have a better way to implement the above scenario please let me know in the comments section down below. I would love to learn/hear from you!
Alright, that is all for today; I had lots of fun, I hope you had fun too. Let’s get one post smarter every day, and see you tomorrow!
P.S For some reason my Reddit account got suspended/banned… While I am trying to reactivate it, do subscribe to email/rss feed for daily updates.
Feel free to show some love in the new comments section too :)
Cheers
Related links
Helper functions
func (t *Tracker) hasCompletedPrerequisites(eventNames []EventName) bool {
t.mu.RLock()
defer t.mu.RUnlock()
for _, event := range eventNames {
if completed := t.completedEvents[event]; !completed {
return false
}
}
return true
}
func (t *Tracker) completeEvent(eventName EventName) {
t.mu.Lock()
defer t.mu.Unlock()
t.completedEvents[eventName] = true
}
func (t *Tracker) hasEventStarted(eventName EventName) bool {
t.mu.RLock()
defer t.mu.RUnlock()
return t.startedEvents[eventName]
}
func (t *Tracker) startEvent(eventName EventName) {
t.mu.Lock()
defer t.mu.Unlock()
t.startedEvents[eventName] = true
}
func (t *Tracker) GetNumStartedEvent() int {
t.mu.RLock()
defer t.mu.RUnlock()
return len(t.startedEvents)
}