Go 专家(04):sync.Cond —— 条件变量用法和常见误区
更新时间:2026-09-01。本文是
languages/go/expert/专家层第 04 篇,接 sync.Map。sync.Cond是 Go 里不太常用但很重要的同步原语——等待"条件满足",唤醒等待者。理解了sync.Cond,才能写对复杂的生产者-消费者场景。
本文要回答的问题
sync.Cond是什么?什么时候用?- 为什么
sync.Cond一定要和 Mutex 一起用? Signal和Broadcast有什么区别?什么时候用哪个?- 常见误区:
if还是for判断条件?
一、基本用法:生产者-消费者
go
var (
mu sync.Mutex
cond = sync.NewCond(&mu)
queue = make([]int, 0, 10)
)
// 生产者
func producer(n int) {
mu.Lock()
for i := 0; i < n; i++ {
queue = append(queue, i)
cond.Signal() // 唤醒一个等待的消费者
}
mu.Unlock()
}
// 消费者
func consumer() {
mu.Lock()
for len(queue) == 0 { // 用 for 循环!不是 if
cond.Wait() // 等待,会释放 mu,唤醒后重新拿锁
}
item := queue[0]
queue = queue[1:]
mu.Unlock()
// 处理 item
}二、sync.Cond 的核心 API
go
// cond = sync.NewCond(&mu) // 绑定 Mutex
cond.Wait() // 等待条件满足,释放 mutex,唤醒后重新拿锁
cond.Signal() // 唤醒一个等待 goroutine
cond.Broadcast() // 唤醒所有等待 goroutine为什么一定要和 Mutex 一起用?
- 条件(
len(queue) == 0)是共享状态,必须加锁保护 Wait()会原子地释放 Mutex,然后进入睡眠——保证不会漏掉 Signal/Broadcast
如果没有 Mutex,生产者往队列里放数据,消费者同时读条件,就会有 data race。
三、for 循环 vs if 判断条件
必须用 for,不能用 if:
go
// ✅ 正确:用 for
mu.Lock()
for len(queue) == 0 {
cond.Wait()
}
// ...
mu.Unlock()
// ❌ 错误:用 if
mu.Lock()
if len(queue) == 0 {
cond.Wait() // 唤醒后,可能条件又不满足了
}
// ...
mu.Unlock()为什么用 for?
- 多个 goroutine 同时被唤醒,第一个消费完,队列可能又空了
- 虚假唤醒(spurious wakeup)——POSIX 线程允许即使没有 Signal/Broadcast,Wait 也可能返回
- 所以
for循环再次检查条件,不满足就继续 Wait
四、Signal vs Broadcast
| 方法 | 唤醒几个 | 适用场景 |
|---|---|---|
Signal() | 一个 | 只有一个消费者能处理数据,比如生产者-消费者队列 |
Broadcast() | 所有 | 状态改变所有等待者都需要知道,比如配置更新、关闭通知 |
go
// Broadcast 例子:关闭队列
func closeQueue() {
mu.Lock()
closed = true
cond.Broadcast() // 唤醒所有等待的消费者,它们都能看到 closed = true
mu.Unlock()
}
// 消费者里:
mu.Lock()
for !closed && len(queue) == 0 {
cond.Wait()
}
if closed {
mu.Unlock()
return
}
// ...五、Wait 的原子性
go
cond.Wait() 的本质:
1. 解锁 Mutex
2. 等待唤醒(原子操作:解锁 + 等待,中间不会漏掉 Signal)
3. 重新加锁 Mutex
4. 返回因为解锁和等待是原子的,所以不会出现"解锁之后,Signal 先来了,然后才进入等待——永远等不到 Signal"这种情况。
六、常见误区
| 坑 | 现象 | 对策 |
|---|---|---|
| 用 if 判断条件 | 虚假唤醒后条件不满足,程序逻辑错 | 永远用 for 循环 |
| Signal 和 Broadcast 选错 | 多个 goroutine 等待,只唤醒一个,其他永远等 | 状态改变要唤醒所有,用 Broadcast |
- 忘记持有 Mutex 调用 Wait | panic | Wait 必须在 mu.Lock() 之后调用 | | 持有 Mutex 调用 Signal/Broadcast | 没问题,对性能影响不大,可以在 Unlock 后调用 | 都可以 |
七、什么时候用 sync.Cond
当你需要:"等待某个条件满足,条件满足后继续执行",用 sync.Cond。
典型场景:
1. 生产者-消费者队列
2. 等待某个 goroutine 完成某个阶段
3. 关闭通知多个 goroutine如果只是简单的"等待所有 goroutine 完成" → sync.WaitGroup 更简单。 如果只是简单的"超时等待" → context + ctx.Done() 更简单。
八、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| if 判断条件 | 虚假唤醒导致程序出错 | 必须用 for |
| 不持有锁调用 Wait | panic | Wait 需要持有锁 |
| 用 Signal 唤醒多个等待者 | 其他 goroutine 永远等下去 | 所有等待者都需要唤醒用 Broadcast |
相关与延伸
下一篇:race detector 原理 —— data race 怎么检测;sync.Cond 底层用信号量,和 C++ 的
std::condition_variable一样,对比见 C++ 并发同步。
一句话总结
sync.Cond:条件变量,配合 Mutex 一起用,用于等待条件满足;Wait() 原子释放 Mutex 然后睡眠,唤醒后重新拿锁;for 循环判断条件,不能用 if;Signal 唤醒一个,Broadcast 唤醒所有;生产者-消费者用 Signal,状态改变唤醒所有人用 Broadcast;理解了 for 循环和原子性,就不会踩坑。