Cond.Wait必须配合for循环使用,因其可能虚假唤醒或条件未满足就返回,需每次唤醒后重新检查条件;Cond.Signal适用于单次事件,Cond.Broadcast适用于状态变更;Cond必须与sync.Mutex或sync.RWMutex配合,Wait前须已持锁。

Cond.Wait 为什么必须配合 for 循环使用
因为 Cond.Wait 可能被虚假唤醒(spurious wakeup),也可能在条件真正满足前就返回。不检查条件直接往下走,大概率读到脏数据或触发逻辑错误。
正确写法是把等待逻辑包在 for 循环里,每次唤醒后重新判断条件是否成立:
for !conditionMet() {
cond.Wait()
}常见错误现象:Cond.Wait 返回后直接消费资源,结果发现队列还是空、信号还没真正就绪、或共享变量仍是旧值。
- 条件判断必须是「可重复执行且无副作用」的纯函数式检查(比如读原子变量、查切片长度、比对标志位)
- 不要在循环体里修改条件依赖的状态——那该放在
Cond.Signal或Cond.Broadcast前做 - 如果条件涉及多个变量,建议用互斥锁保护后一次性读取,避免中间态被其他 goroutine 扰动
Cond.Signal 和 Cond.Broadcast 的选择依据
Cond.Signal 只唤醒一个等待中的 goroutine,Cond.Broadcast 唤醒全部。选哪个,取决于你协调的是「单次事件」还是「状态变更」。
立即学习“go语言免费学习笔记(深入)”;
典型场景:
- 生产者往缓冲队列 push 一项 —— 用
Cond.Signal,只需通知一个消费者即可 - 关闭全局服务、重载配置、广播停止信号 —— 用
Cond.Broadcast,所有监听者都得响应 - 误用
Cond.Broadcast在高频事件中会引发惊群效应,大量 goroutine 竞争锁后又发现条件不满足,白白消耗调度开销
性能影响:在数百 goroutine 等待时,Cond.Broadcast 的唤醒+调度成本明显高于 Cond.Signal;但若只用 Signal 却需要唤醒多个协程,则可能造成部分 goroutine 长时间饥饿。
Cond 必须和 *sync.Mutex 或 *sync.RWMutex 配合使用
Cond 本身不带锁,它只是条件通知的“喇叭”,真正的状态保护得靠外部锁。官方文档明确要求:调用 Wait 前必须已持有锁,且该锁会在进入等待时自动释放,并在唤醒后自动重新获取。
常见错误:
- 传入一个未加锁的
*sync.Mutex给sync.NewCond—— 运行时不会报错,但Wait行为不可预测 - 在
Wait返回后,没再次加锁就访问共享状态 —— 出现 data race,go test -race 会立刻捕获 - 用
sync.RWMutex时,传给sync.NewCond的是指针,但调用Wait前必须用Lock()(不是Rlock()),因为Wait内部要 unlock,而RUnlock不能匹配Rlock的语义
正确模式固定为:mu.Lock(); defer mu.Unlock() 包裹整个条件检查 + Wait 流程。
替代方案:什么时候该放弃 Cond 改用 channel
如果你的协调模型天然是一对一、有明确生命周期、或者只需要传递简单信号,channel 更简洁、更不容易出错。
例如:
- 等待某个后台任务完成 —— 用
done chan struct{}+close(done)比 Cond 更直观 - 限流器中通知下一个请求可以执行 ——
sem 天然带排队语义,无需手动管理锁和条件 - Cond 容易漏掉 Signal(尤其在 error 分支里忘了 notify),而 channel 的发送是显式的、可静态检查的
但 channel 不适合「一对多动态等待」或「复杂条件组合」(比如“当 A 为真且 B 超过阈值时唤醒”),这时 Cond 配合清晰的状态变量仍是更可控的选择。
真正容易被忽略的点是:Cond 的条件变量必须是「应用层可维护的确定状态」,而不是靠 side effect 推导出来的隐式结果。一旦条件逻辑开始嵌套判断、依赖外部 IO 或耗时计算,就该警觉——这已经超出 Cond 的设计边界了。










