If you ask a Go developer how to handle concurrency, they will almost certainly say: "Use Channels."
And 95% of the time, they are right. Channels are the idiomatic way to send data and signals between goroutines. But what about that other 5%? What happens when you need to broadcast a signal to 1,000 goroutines simultaneously without looping?
Enter sync.Cond (the Condition Variable)—Go’s most misunderstood concurrency primitive.
In this post, I’ll explain sync.Cond using a simple mental model: The Dinner Bell.
The Problem: Polling vs. Signaling
Imagine a father (The Publisher) making pancakes for his 10 hungry children (The Subscribers).
Approach 1: The Polling Loop (Bad)
The children run into the kitchen every 5 seconds, check the plate, see it's empty, and leave.
- CPU: High (Children are running back and forth).
- Contention: The kitchen door (Mutex) is constantly being locked and unlocked.
Approach 2: Channels (Okay, but Linear)
The father finishes a pancake. He has to walk to each child individually and hand them a piece.
- Latency: The 10th child gets their food much later than the 1st.
- Coupling: The father is busy delivering instead of cooking.
Approach 3: sync.Cond (The Dinner Bell)
The children sit at the table and fall asleep.
The father cooks a batch of pancakes, puts them on the table, and rings a loud bell (Broadcast).
- Result: Everyone wakes up instantly.
- Efficiency: Zero CPU usage while waiting.
How it Works in Code
The sync.Cond object is always paired with a sync.Mutex (the lock). This is the part that confuses most developers.
Think of the Mutex as the Key to the Kitchen. You cannot check for food (Data) without the Key.
Here is the pattern:
package main
import (
"fmt"
"sync"
"time"
)
type Kitchen struct {
mu sync.Mutex
cond *sync.Cond
pancakes int
}
func NewKitchen() *Kitchen {
k := &Kitchen{}
// We link the Cond to the Lock!
k.cond = sync.NewCond(&k.mu)
return k
}
The Publisher (The Cook)
The cook creates data and rings the bell.
func (k *Kitchen) Cook() {
k.mu.Lock() // 1. Grab the Key
k.pancakes++ // 2. Make food
fmt.Println("Pancake ready!")
k.mu.Unlock() // 3. Put Key back
// 4. RING THE BELL!
// Note: We don't need to hold the lock to broadcast,
// but it's often safer to do so.
k.cond.Broadcast()
}
The Subscriber (The Hungry Child)
This is where the magic happens. Look closely at the Wait() call.
func (k *Kitchen) Eat(id int) {
k.mu.Lock() // 1. Grab Key to enter kitchen
defer k.mu.Unlock()
// 2. The Check Loop
// Why a loop? Because when you wake up, someone else might
// have eaten the pancake before you!
for k.pancakes == 0 {
// 3. WAIT
// This line does three things atomically:
// A. Unlocks the mutex (Drops the key).
// B. Suspends execution (Falls asleep).
// C. Locks the mutex (Grabs key) when woken up.
k.cond.Wait()
}
// 4. Eat
k.pancakes--
fmt.Printf("Child %d ate a pancake.\n", id)
}
The "Gotcha": Why does Wait need a Lock?
The most common question I get is:
"Why do I have to pass the lock to
sync.NewCond? And why must I hold the lock before callingWait?"
Go back to the analogy.
If Wait() didn't drop the lock for you, you would fall asleep inside the kitchen with the door locked! The Cook would never be able to get in to make the food.
sync.Cond.Wait() performs a magic trick: it creates a safe point where you say, "I am done with the lock for now, wake me up when something changes."
When should you use this?
Don't abandon Channels yet. Use sync.Cond only when:
- Multiple Readers: You have many goroutines waiting for the same signal.
- State-Based: You are waiting for a specific condition (e.g., "Buffer is full", "Server is ready"), not just passing a value.
- High Frequency: You want to avoid the overhead of creating/closing channels repeatedly.
Summary
- Channels are for passing data. (Mailman)
- Mutexes are for protecting data. (Lock and Key)
- Conditions are for signaling state changes. (Dinner Bell)
Mastering sync.Cond places you in the upper echelon of Go developers who understand that "Don't communicate by sharing memory" is a guideline, not a dogma. Sometimes, sharing memory (with the right locks) is exactly what you need for performance.
Thanks for reading! If you have any war stories about sync.Cond or deadlocks, let me know in the comments below.
Top comments (0)