In Go, how do sync.Cond.Signal and Broadcast differ, and must c.L be held to call them?
answer
- one versus all
- nothing is queued if nobody waits
- the lock is optional for the notifier
- shutdown concerns every waiter
- the mutation, not the wake-up, must be under c.L
basics
~10 sSignal wakes at most one goroutine parked in Wait on that sync.Cond; Broadcast wakes every one of them. Neither requires holding c.L, though the state change being announced must itself be made under c.L.
solid answer
~50 s`Signal` wakes at most one goroutine currently parked in `Wait` on that `*sync.Cond`, and the docs do not say which one; `Broadcast` wakes all of them. Both are no-ops if nobody is waiting — a wake-up is not queued or remembered, which is why the truth lives in the lock-protected state and not in the notification. Go explicitly allows, but does not require, holding `c.L` when you call either one: the mutation you are announcing must happen under `c.L`, but the `Signal` or `Broadcast` itself can come after you unlock. If you call it while still holding the lock, the woken goroutines simply block re-acquiring `c.L` until you release it, which costs a little but is correct. In practice: `Signal` when you made exactly one unit of work available and any single waiter can take it; `Broadcast` when you changed something every waiter must observe, such as flipping a queue to closed.
code
go · 13 linesfunc (q *queue) put(v string) {
q.mu.Lock()
q.items = append(q.items, v)
q.mu.Unlock()
q.notEmpty.Signal() // one new item, so one waiter is enough
}
func (q *queue) close() {
q.mu.Lock()
q.closed = true
q.mu.Unlock()
q.notEmpty.Broadcast() // every parked consumer must observe closed
}go deeper
Remember the headline: Signal wakes at most one parked goroutine, Broadcast wakes all of them, and neither hands over any data — the waiter still has to read the shared state itself.
Be ready to state that Go allows but does not require holding c.L when notifying, that the state change must nevertheless happen under c.L, and that a notification with no waiters is simply dropped.
Show judgment about which to use in a running system: Broadcast for anything every waiter must observe such as close or shutdown, and a second condition variable per predicate when Broadcast storms start showing up in profiles.
Own the API-shape call: whether the concurrency inside a shared package is expressed with condition variables at all, given the maintenance cost of reasoning about who wakes whom versus a channel-based design other teams read faster.
## The two notification calls `sync.Cond` offers exactly two ways to wake goroutines that are parked in `Wait`: - `func (c *sync.Cond) Signal()` — wakes **at most one** waiting goroutine. - `func (c *sync.Cond) Broadcast()` — wakes **all** waiting goroutines. Both take no arguments, return nothing, and carry no payload. They are pure notifications: they say "go look at the state again", never "here is a value". This is the structural difference from a channel, which transports a value. ### Neither call is remembered If no goroutine is parked at the moment you call `Signal` or `Broadcast`, the call does nothing at all. It is not buffered, not queued, and not delivered to the next goroutine that happens to `Wait` a microsecond later. This is why the shared, lock-protected state is the only source of truth. A consumer that locks, sees an item, and takes it never needed a signal; a consumer that locks, sees nothing, and parks will be woken by the *next* notification. Because the check and the park both happen under `c.L`, and `Wait` releases the lock atomically with parking, a producer that mutates under `c.L` and then notifies cannot slip a notification into the gap and lose it. ### Which one does `Signal` wake? The documentation says only "one goroutine, if any". Do not build anything on a particular ordering or fairness property — treat the choice as unspecified, and make every waiter's code correct regardless of which one is picked. ## Holding `c.L` or not This is the Go-specific detail interviewers probe. The `sync` docs say it is **allowed but not required** for the caller to hold `c.L` during a call to `Signal` or `Broadcast`. Both shapes are legal: ``` q.mu.Lock() q.items = append(q.items, v) q.mu.Unlock() q.notEmpty.Signal() ``` and ``` q.mu.Lock() q.items = append(q.items, v) q.notEmpty.Signal() q.mu.Unlock() ``` What is **not** optional is that the mutation itself — appending the item, flipping a flag — happens under `c.L`. That is what makes the waiter's check-then-park sequence safe. The practical difference between the two shapes is small. Notifying while still holding the lock means the woken goroutines immediately block trying to re-acquire `c.L` inside `Wait`, and only make progress once you unlock; notifying after unlocking lets them proceed straight away. Either is correct. Notifying after unlocking is the more common style, and it keeps the critical section as narrow as the state change itself. ## Choosing between them The honest rule of thumb in Go code: - Use `Signal` when you have made **one** unit of work available and **any single** parked goroutine can consume it, and every waiter is parked on the same predicate. One item appended to a queue whose only waiters are identical consumers is the textbook case. - Use `Broadcast` when the state change is **relevant to every waiter** — most obviously a shutdown or close, where each parked goroutine must wake up, notice the new flag and return. `Signal` there would release one goroutine and leave the rest parked with nobody left to wake them. - Use `Broadcast` when different goroutines are parked on **different predicates** on the same `Cond` (for example a bounded queue where producers wait for space and consumers wait for items and both use one condition variable). `Signal` cannot choose which kind of waiter it hits. When `Broadcast` is used on a hot path with many waiters it produces a burst of goroutines that all wake, re-acquire the lock one at a time, and mostly find nothing to do. That cost is real but usually small compared to getting the wake-up logic wrong; if it matters, the fix is usually two condition variables over the same mutex — say `notEmpty` and `notFull` — each with its own single-predicate waiter set, so `Signal` becomes correct again. ## Testing that you got it right The behaviour is directly testable, which is worth showing in an interview: start N goroutines that each increment a counter under the lock and then `Wait`, block until the counter reaches N so you know they are all parked, then call `Broadcast` and require all N to return. The same harness with `Signal` in place of `Broadcast` shows exactly one returning and the rest still parked — which is the difference stated as an executable fact rather than a claim.
- What does Signal do when no goroutine is currently parked in Wait?Nothing. The notification is not buffered or remembered, so a `Signal` that arrives before anyone waits is simply gone. That is safe only because the state it announces was changed under `c.L`: a goroutine arriving later locks, sees the new state, and never parks in the first place.
- Is it a bug to call Broadcast while still holding c.L?No, it is explicitly allowed. The woken goroutines just block inside `Wait` re-acquiring `c.L` until you release it, so you pay a little extra scheduling churn. Many people unlock first to keep the critical section tight, but both orders are correct as long as the state change itself happened under the lock.
- How would you write a test that proves Broadcast wakes every waiter and Signal does not?Start N goroutines that increment a parked-counter under the mutex and then `Wait`. Spin until the counter reaches N so you know all are parked, then call `Broadcast` and require all N to return within the test's deadline. Re-run the same harness with `Signal` and assert that exactly one returns.
- Two waiter kinds share one sync.Cond — producers waiting for space, consumers waiting for items. What breaks with Signal?`Signal` picks one parked goroutine and the API gives you no say in which kind it is, so a producer's notification can land on another producer and the consumer that could have made progress stays asleep. Either use `Broadcast`, or give each predicate its own `*sync.Cond` over the same mutex.
saying these in an interview costs you the question
- Says Signal is just a faster Broadcast
- Thinks a Signal is queued until someone waits
- Claims c.L must be held to call Signal or Broadcast
- Believes Signal wakes the longest-waiting goroutine by contract
- Uses Signal to announce a shutdown flag
- Expects Signal or Broadcast to carry a value to the waiter