Go의 sync.Cond 잠금 해제: 디너 벨 패턴
Source: Dev.to
The Dinner Bell Analogy
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.
- CPU: high (children are running back and forth).
- Contention: the kitchen door (Mutex) is constantly being locked and unlocked.
-
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.
Now, let the children sit at the table and fall asleep. The father rings a loud bell (Broadcast).
- Result: Everyone wakes up instantly.
- Efficiency: Zero CPU usage while waiting.
In Go, sync.Cond (the Condition Variable) plays the role of that dinner bell. It is always paired with a sync.Mutex (the Key to the Kitchen). You cannot check for food (data) without the key.
The Pattern
package main
import (
"fmt"
"sync"
"time"
)
type Kitchen struct {
mu sync.Mutex
cond *sync.Cond
pancakes int
}
func NewKitchen() *Kitchen {
k := &Kitchen{}
// Link the Cond to the lock
k.cond = sync.NewCond(&k.mu)
return k
}
// 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!
// It’s safe to broadcast without holding the lock,
// but doing so is often clearer.
k.cond.Broadcast()
}
// Each child tries to eat a pancake.
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 after waking up, another child may have
// already taken the pancake.
for k.pancakes == 0 {
// 3. WAIT
// 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)
}
Why Wait Needs the Lock
If Wait didn’t release the lock, a goroutine would fall asleep while holding the kitchen door closed. The cook would never be able to enter and make food. sync.Cond.Wait() performs a “magic trick”: it temporarily gives up the lock, sleeps, and reacquires the lock when signaled.
When to Use sync.Cond
- Multiple readers – many goroutines wait for the same signal.
- State‑based waiting – you need to wait for a condition such as “buffer is full” or “server is ready”, not just a value.
- High‑frequency signaling – you want to avoid the overhead of repeatedly creating and closing channels.
| Primitive | Primary purpose |
|---|---|
| Channels | Passing data (mailman) |
| Mutexes | Protecting data (lock & key) |
| Conditions | Signaling state changes (dinner bell) |
TL;DR
sync.Cond is not a replacement for channels; it complements them when you need efficient broadcast of state changes. Mastering it moves you beyond “don’t communicate by sharing memory” and lets you choose the right tool for the job.
Thanks for reading! If you have war stories about sync.Cond or deadlocks, feel free to share them in the comments.