sync.Cond 必须与 *sync.Mutex 配合使用,Wait 前需持锁,条件检查必须用 for 循环,Signal/Broadcast 依语义选择;它适用于同一锁下多条件协同,非简单通信场景。

sync.Cond 不能单独用,必须和 *sync.Mutex 配合、用 for 循环检查条件、唤醒后仍需重新验证——跳过任一环节,大概率出现假唤醒、死锁或漏通知。
为什么 cond.Wait() 必须在 mu.Lock() 之后调用
cond.Wait() 内部会原子性地释放传入的锁,挂起 goroutine,被唤醒后再重新获取该锁。这个“释放→挂起→唤醒→重锁”流程只有在调用前已持锁的前提下才安全。否则运行时 panic:sync: Cond.Wait with uninitialized mutex,或更隐蔽地触发竞态。
- 锁不是可选配件,是
sync.Cond正常工作的前提;cond.L字段必须指向一个已初始化的*sync.Mutex(不支持*sync.RWMutex) - 所有对共享状态(如
dataReady、queue.Len())的读写,都必须在mu.Lock()/mu.Unlock()之间完成 - 别在锁外读状态再进 Wait:比如先
if !dataReady再锁、再 Wait,中间存在竞态窗口,可能刚读完false,另一协程就设成true并 Signal,结果你还是进了 Wait
为什么必须用 for !condition { cond.Wait() } 而不是 if
Go 不保证 cond.Wait() 返回时条件一定成立。虚假唤醒(spurious wakeup)是运行时允许的行为,且 Signal() 可能唤醒了还没来得及进入 Wait 的 goroutine,导致通知丢失。只靠 if 判断,一醒就执行逻辑,极大概率读到脏数据或触发 panic。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 正确模式:
mu.Lock(); for !dataReady { cond.Wait() }; processData(); mu.Unlock() - 每次
Wait()返回,都意味着锁已被重新持有,此时可安全再次检查条件 - 即使你 100% 确信不会虚假唤醒,Go 官方文档也强制要求循环检查——这不是优化建议,是并发安全的硬约束
Signal() 和 Broadcast() 怎么选
选哪个不看“力度”,而看语义:状态变更是否对所有等待者都生效。
立即学习“go语言免费学习笔记(深入)”;
-
Signal():唤醒**至多一个**等待 goroutine。适合“一次性消费”场景,如任务队列有新任务、资源池归还一个空闲连接——唤醒一个 worker 处理即可 -
Broadcast():唤醒**所有**等待 goroutine。适合“全局状态变更”场景,如服务关闭标志置为true、配置热更新完成、缓存批量失效——每个等待者都需要立刻退出或重载 -
Signal()不排队、不累积:如果当前没 goroutine 在 Wait,信号直接丢弃;Broadcast()开销略大,但比漏唤醒导致永久阻塞强得多
和 channel 比,sync.Cond 什么时候更合适
sync.Cond 不传数据,只发信号;它不替代 channel,而是解决 channel 不擅长的场景:同一把锁保护多个条件、状态复杂且需精细唤醒控制。
- 典型优势场景:一个服务同时监控连接数、内存水位、磁盘空间三个阈值,分别用
connCond、memCond、diskCond,共用一把mu,各自Wait/Signal,互不干扰 - 不适合简单点对点通信:比如“生产者发一个值,消费者收一个值”,用
chan T更直观、更安全 - 真正难的是设计:状态变量怎么定义、谁负责修改、何时
Signal、谁该Broadcast——错一点,要么忙等,要么永远卡住,且问题往往延迟暴露

















