Go의 sync.Cond 잠금 해제: 디너 벨 패턴

발행: (2025년 12월 30일 오전 03:51 GMT+9)
3 min read
원문: Dev.to

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.
PrimitivePrimary purpose
ChannelsPassing data (mailman)
MutexesProtecting data (lock & key)
ConditionsSignaling 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.

Back to Blog

관련 글

더 보기 »