wait 必须在 for 循环中调用以应对虚假唤醒,确保每次唤醒后重新检查条件;signal 仅唤醒一个等待 goroutine 且不累积,需在状态更新后、锁外调用。

Wait 和 Signal 不是独立可用的信号对,必须和 sync.Mutex 绑定使用,且 Wait 必须在锁已持有、条件检查完成之后调用——否则直接 panic。
为什么 Wait 必须写在 for 循环里?
因为唤醒可能是虚假的(spurious wakeup):系统调度或内核行为可能让 goroutine 在没收到 Signal 的情况下就返回。Go 不保证每次 Wait 返回都对应真实的状态变化。
所以不能这么写:
mu.Lock()
if len(data) == 0 {
cond.Wait() // ❌ 错误:醒来后 data 仍可能为空
}
// 后续操作假设 data 非空 → 可能 panic
而要这样写:
mu.Lock()
for len(data) == 0 {
cond.Wait() // ✅ 正确:每次醒来都重新检查
}
// 此时 data 一定非空,可安全消费
value := data[0]
data = data[1:]
mu.Unlock()
-
Wait返回时锁已自动重新获取,不需要手动mu.Lock() - 条件检查(如
len(data) == 0)和Wait必须在同一个锁保护下,否则检查完、Wait前状态可能被其他 goroutine 改掉 - 即使你“确定不会虚假唤醒”,Go 官方文档也强制要求循环检查
Signal 唤醒的是哪个 goroutine?
Signal 确实只唤醒一个正在 Wait 的 goroutine,但具体唤醒哪一个由 Go 调度器决定,不可预测、也不应依赖。
它适合的场景是:共享状态变更只影响一个等待者,比如任务队列有新任务,只需唤醒一个空闲 worker:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 多个 worker 协程在
for queue.Len() == 0 { cond.Wait() }中等待 - 生产者入队后调用
cond.Signal()→ 恰好一个 worker 被唤醒并取走任务 - 如果用
Broadcast,所有 worker 都会争抢锁和任务,造成不必要的竞争
但要注意:Signal 不会排队、不累积。如果调用时没人等待,这次通知就丢了——它不是 channel,没有缓冲区。
常见 panic 场景与修复方式
以下错误代码会导致运行时 panic:
cond := sync.NewCond(&sync.Mutex{})
cond.Wait() // panic: sync: Cond.Wait with uninitialized mutex
原因和修复点:
- 没在调用
Wait前加锁 → 必须先mu.Lock() - 用了
sync.RWMutex而不是*sync.Mutex→Cond只接受sync.Locker,但内部实现只兼容*sync.Mutex;*sync.RWMutex传进去会 panic 或行为异常 - 复制了已使用的
Cond实例 →Cond类型包含noCopy字段,复制后首次Wait就 panic -
Signal/Broadcast时锁未持有?其实不用持锁,但必须确保此时共享状态已更新完毕且线程安全(通常在锁内改完状态,再解锁后调用Signal)
Wait 和 Signal 的典型协作节奏
正确顺序不是“先锁 → Wait → Signal → 解锁”,而是:
- 消费者:持锁 → 检查条件 → 不满足则
Wait(自动释放锁)→ 被唤醒后锁已重入 → 消费 → 解锁 - 生产者:持锁 → 更新共享状态(如 append 到 slice)→ 解锁 → 调用
cond.Signal()
这个节奏避免了生产者在锁内做耗时操作,也防止消费者在 Wait 前被抢占导致状态检查失效。最易忽略的一点是:很多人把 Signal 放在锁内,看似“更安全”,实则延长了锁持有时间,反而降低并发吞吐——只要状态更新本身是原子的(如单个赋值、slice append),Signal 完全可以放在锁外。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










