wait被唤醒后必须用for循环重新检查条件,因虚假唤醒和竞态可能导致条件仍不满足;broadcast会唤醒所有等待者引发惊群效应,应优先用signal并确保锁与cond生命周期一致。

Wait 被唤醒后必须重新检查条件,否则可能读到过期状态
sync.Cond 的 Wait 不保证唤醒时条件一定成立——它只表示“可能成立了”。这是因为存在虚假唤醒(spurious wakeup)和条件竞争:另一个 goroutine 可能在你被唤醒、但还没重新加锁前就改回了旧值。
所以正确写法永远是 for !condition { c.Wait() },而不是 if !condition { c.Wait() }。比如消费者检查 len(queue) == 0,即使被 Broadcast 唤醒,也可能刚被其他 consumer 抢先取走最后一项,导致下标越界。
- 错误示例:
if len(queue) == 0 { c.Wait() }→ 一次唤醒后直接读queue[0],panic - 正确模式:循环内检查 +
Wait,确保每次进入临界区前条件真实满足 - 这个循环不是性能开销,而是并发安全的强制契约
Broadcast 唤醒全部等待者会触发惊群效应
Broadcast 本身不持有锁,但它会把所有在 Wait 中挂起的 goroutine 全部推入运行队列。这些 goroutine 醒来后第一件事就是争抢关联锁(c.L),造成瞬时锁竞争高峰。
典型表现是:CPU 使用率突增、goroutine 调度延迟上升、实际吞吐反而下降——尤其当等待者数量超过 10 个时更明显。
- 适用场景:
Broadcast仅适合“所有等待者都该同时推进”的情况,例如关闭信号、全局配置刷新 - 替代方案:优先用
Signal,配合生产者/消费者逻辑中“每产生一项数据只唤醒一个消费者”的语义 - 注意:
Signal并不保证唤醒“最早等待的那个”,只是唤醒“任意一个”,所以不能依赖唤醒顺序做业务逻辑
Wait 内部自动释放并重获锁,但锁必须由调用方显式提供
Wait 的行为依赖于它绑定的 Locker(通常是 *sync.Mutex)。它会在挂起前调用 L.Unlock(),被唤醒后立即调用 L.Lock()。这意味着:
- 调用
Wait前必须已持有该锁,否则 panic(runtime error: sync: inconsistent mutex state) - 不能把
Wait放在 defer 后面,也不能在未加锁时直接调用 - 锁对象生命周期必须长于
Cond实例;复制已使用的Cond会导致未定义行为
惊群效应的真正瓶颈不在 Cond 本身,而在共享状态的锁粒度
很多人以为优化方向是换掉 Broadcast,但更关键的是:为什么有那么多 goroutine 在等同一个条件?这往往暴露了锁范围过大或状态建模不合理。
例如,用一个全局 sync.Cond 等待“任意缓冲区有数据”,不如为每个 worker 分配独立 channel 或细粒度条件变量 + 分片锁。
- 高并发下,
Broadcast是症状,不是病根 - 减少等待者数量比优化唤醒策略更有效
- 如果必须用
Broadcast,确保唤醒后的临界区极短(只做标记或发消息,不执行耗时操作)
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











