go语言中cond.wait()必须在持有互斥锁时调用,因其内部原子执行unlock→sleep→re-lock,未持锁调用会panic;条件检查须在锁内完成并用for循环防虚假唤醒,且sync.cond需用sync.newcond初始化,不可复制或复用锁。

Go 语言里没有独立的“条件变量等待函数”,sync.Cond 的等待必须配合 sync.Mutex 手动实现,直接调用 Cond.Wait() 而不加锁会 panic。
为什么 Cond.Wait() 必须在持有互斥锁时调用
Cond.Wait() 的设计是原子的:它先释放锁、挂起 goroutine,等被唤醒后再重新获取锁。如果调用前没持锁,运行时会立即 panic:sync: Cond.Wait called without holding mutex。这不是文档疏漏,而是强制要求——没有锁保护的条件检查本身就不安全。
- 条件判断(比如
queue == nil)必须在锁内完成,否则可能刚判完条件就变,导致错失唤醒或虚假唤醒 -
Cond.Wait()内部会自动 unlock → sleep → re-lock,省去手动解锁/重锁逻辑,但前提是 caller 已 lock - 常见错误:在
if判断后 unlock,再调Cond.Wait()—— 这会导致竞态和 panic
标准写法:for 循环 + 锁内判断 + Cond.Wait()
条件变量天然要处理虚假唤醒(spurious wakeup),所以永远要用 for 循环包裹条件检查,而不是 if。典型模式如下:
mu.Lock()
defer mu.Unlock() // 注意:不能 defer 在 Wait 前!
for len(queue) == 0 {
cond.Wait() // 自动释放 mu,唤醒后自动重新持有 mu
}
// 此时 queue 非空,且 mu 仍被持有
item := queue[0]
queue = queue[1:]
-
defer mu.Unlock()必须放在cond.Wait()之后,否则锁会在 Wait 前就被释放,触发 panic - 循环条件必须是“业务条件”,不是
!cond.Signal()这类错误逻辑 - 唤醒后必须再次检查条件,因为可能被其他 goroutine 消费了资源,或被虚假唤醒
Cond.Signal() 和 Cond.Broadcast() 的选择依据
Signal() 只唤醒一个等待者,Broadcast() 唤醒全部。选哪个取决于业务语义:
- 生产者入队一个 item,只用
cond.Signal()—— 只需唤醒一个消费者即可 - 关闭通道、重置状态、广播终止信号,用
cond.Broadcast() - 误用
Broadcast()在高并发下可能引发“惊群”,但 Go 的sync.Cond实现已优化唤醒调度,实际影响有限;真正瓶颈通常是业务逻辑而非唤醒本身 - 注意:
Signal()不保证唤醒“最先等待”的 goroutine,Go 调度器决定唤醒顺序
容易被忽略的初始化与零值陷阱
sync.Cond 是不可复制类型,且零值无效。以下写法都会出问题:
// ❌ 错误:零值 Cond,Wait 会 panic
var cond sync.Cond
cond.Wait() // panic: sync: Cond.Wait on uninitialized Cond
// ✅ 正确:必须用 sync.NewCond 创建
mu := &sync.Mutex{}
cond := sync.NewCond(mu)
// ❌ 错误:嵌入结构体时未显式初始化
type Service struct {
mu sync.Mutex
cond sync.Cond // 零值!
}
// 使用前必须:s.cond = *sync.NewCond(&s.mu)
- 不要对
sync.Cond字段做匿名嵌入并期望自动初始化 - 不要传递
sync.Cond值(复制),它包含内部指针,复制后行为未定义 - 不要在多个不同
sync.Mutex上复用同一个sync.Cond—— 它绑定创建时传入的锁
真正难的不是写对 Wait(),而是把条件检查、锁生命周期、唤醒语义三者对齐。多线程下“条件成立”本身就是一个需要锁保护的状态,而 Cond 只是帮你管理等待队列——它不替你思考业务逻辑是否完备。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











