sync.NewCond的核心用途是协调多个goroutine在共享状态满足特定条件时的等待与唤醒,需配合sync.Mutex使用,所有操作必须在锁保护下进行,且Wait必须置于for循环中以防虚假唤醒。

sync.NewCond 的核心用途是啥
sync.NewCond 不是用来替代 chan 的,它本身不传递数据,只负责“条件满足时唤醒等待者”。在生产者消费者模型中,它常和 sync.Mutex 配合,解决“缓冲区空时消费者等、满时生产者等”这类**状态依赖型等待**问题。直接用 channel 也能做,但如果你需要更细粒度的控制(比如多个条件共用一把锁、或需广播而非单次唤醒),sync.NewCond 就绕不开。
它必须绑定一个已存在的 sync.Locker(通常是 *sync.Mutex),且所有对共享状态的读写、以及 Wait/Signal/Broadcast 调用,都必须在该锁保护下进行——漏锁或错锁,会直接导致死锁或竞态。
为什么 Wait 必须在 for 循环里调用
Cond.Wait 在返回前会自动释放锁,并在被唤醒后重新获取锁。但唤醒不等于条件已满足:可能被虚假唤醒(spurious wakeup),也可能其他 goroutine 抢先修改了状态。所以不能写成 if !condition { cond.Wait() }。
正确写法是:
mu.Lock()
defer mu.Unlock()
for len(buffer) == 0 {
cond.Wait()
}
// 此时 buffer 一定非空
- 每次
Wait返回,都要重新检查条件,不能假设“醒了就是有数据” - 如果用
if,可能刚醒就被另一个消费者取走最后一项,当前 goroutine 接着读就会 panic - 循环也避免了在条件不满足时意外跳过等待
Signal 和 Broadcast 怎么选
cond.Signal() 唤醒**一个**正在 Wait 的 goroutine;cond.Broadcast() 唤醒**所有**等待者。
生产者放完一项数据,通常只需唤醒一个消费者:cond.Signal() 更高效,避免惊群。但以下情况必须用 cond.Broadcast():
- 你维护多个条件变量(比如
notFull和notEmpty)但共用一把锁,且修改状态可能同时影响多个条件 - 消费者逻辑中存在“跳过本次处理”的分支(如过滤掉某些 item),导致唤醒一个后它又立刻回去等
- 你无法确定哪个 goroutine 等待的是当前变化相关的条件(尤其在复杂状态机中)
常见错误是:生产者调用 Signal 后,发现没 consumer 响应——其实是因为所有 consumer 都还没开始 Wait,或者刚被唤醒就因条件不满足又进了下一轮 Wait。这时候加日志打点比盲目换 Broadcast 更有用。
一个最小可运行的双条件示例
下面是一个带容量限制的 buffer,用两个 *sync.Cond 分别管理“非空”和“非满”:
type BoundedBuffer struct {
mu sync.Mutex
condFull *sync.Cond
condEmpty *sync.Cond
data []int
cap int
}
func NewBoundedBuffer(n int) *BoundedBuffer {
mu := &sync.Mutex{}
return &BoundedBuffer{
mu: *mu,
condFull: sync.NewCond(mu),
condEmpty: sync.NewCond(mu),
cap: n,
}
}
func (b *BoundedBuffer) Produce(item int) {
b.mu.Lock()
defer b.mu.Unlock()
for len(b.data) == b.cap {
b.condFull.Wait() // 满了,等消费者取走
}
b.data = append(b.data, item)
b.condEmpty.Signal() // 通知至少一个消费者:有数据了
}
func (b *BoundedBuffer) Consume() int {
b.mu.Lock()
defer b.mu.Unlock()
for len(b.data) == 0 {
b.condEmpty.Wait() // 空了,等生产者放入
}
item := b.data[0]
b.data = b.data[1:]
b.condFull.Signal() // 通知至少一个生产者:有空位了
return item
}
注意:两个 Cond 共享同一个 *sync.Mutex,这是合法且必要的;Signal 调用发生在锁内,确保唤醒和状态更新的原子性;append 和切片操作都在锁保护下,避免并发读写 slice header。
真正容易被忽略的是:Cond 的生命周期必须长于所有使用它的 goroutine。不要在函数栈上创建 sync.Cond 并传给 goroutine——它会被回收,后续 Wait 会 panic。务必保证它是包级变量、结构体字段,或通过指针安全传递。


















