Tools
Unlocking Go’s sync.Cond: The Dinner Bell Pattern
2025-12-29
0 views
admin
The Problem: Polling vs. Signaling ## Approach 1: The Polling Loop (Bad) ## Approach 2: Channels (Okay, but Linear) ## Approach 3: sync.Cond (The Dinner Bell) ## How it Works in Code ## The Publisher (The Cook) ## The Subscriber (The Hungry Child) ## The "Gotcha": Why does Wait need a Lock? ## When should you use this? ## Summary 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. Imagine a father (The Publisher) making pancakes for his 10 hungry children (The Subscribers). The children run into the kitchen every 5 seconds, check the plate, see it's empty, and leave. The father finishes a pancake. He has to walk to each child individually and hand them a piece. 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). 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. The cook creates data and rings the bell. This is where the magic happens. Look closely at the Wait() call. 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 calling Wait?" 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." Don't abandon Channels yet. Use sync.Cond only when: 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. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
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
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
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
} CODE_BLOCK:
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
} CODE_BLOCK:
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()
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
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()
} CODE_BLOCK:
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()
} CODE_BLOCK:
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)
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
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)
} CODE_BLOCK:
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)
} - CPU: High (Children are running back and forth).
- Contention: The kitchen door (Mutex) is constantly being locked and unlocked. - Latency: The 10th child gets their food much later than the 1st.
- Coupling: The father is busy delivering instead of cooking. - Result: Everyone wakes up instantly.
- Efficiency: Zero CPU usage while waiting. - 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. - Channels are for passing data. (Mailman)
- Mutexes are for protecting data. (Lock and Key)
- Conditions are for signaling state changes. (Dinner Bell)
how-totutorialguidedev.toaiserver