下面来详细拆解 sync.Cond 条件变量。
一、什么是条件变量,它解决什么问题?
互斥锁 (sync.Mutex) 解决的是互斥进入临界区的问题,但它本身没法表达 “等待某个条件成立” 这种语义。
当 goroutine 发现条件不满足时,一般只能两种做法:
- 忙等:循环检查条件占用 CPU;
- 定时休眠,但总会有不必要的延迟或唤醒。
条件变量就是为了高效解决这个问题:
在互斥锁保护下,检查条件;条件不满足时原子地释放锁并挂起 goroutine;条件满足时被唤醒,重新获取锁,继续执行。
sync.Cond 与 sync.Mutex 或 sync.RWMutex 配合,提供 Wait / Signal / Broadcast 三个方法,实现这种等待/通知机制。
二、创建与接口
type Cond struct {noCopy noCopy // go vet 禁止复制L Locker // 关联的锁,通常是 *Mutex 或 *RWMutexnotify notifyList // 内部等待链表checker copyChecker // 运行时复制检测(go vet 静态分析也会查)
}
通过 sync.NewCond(l Locker) 创建,参数 l 必须是 Locker 接口(实现 Lock() 和 Unlock()),一般传 *sync.Mutex 或 *sync.RWMutex。
*sync.Cond是唯一的合理使用方式,禁止复制值。- 内部持有 goroutine 等待队列;复制会导致独立的等待队列,原、副本的
Signal/Broadcast无法唤醒对方队列中的 goroutine。
三、核心方法
1. Wait() —— 等待条件满足
调用时必须持有锁(L.Lock() 已调用),否则会 panic(Go 1.6+)。
Wait() 执行以下原子操作:
- 将当前 goroutine 加入该
Cond的等待队列; - 调用
L.Unlock()释放锁; - 阻塞当前 goroutine,等待被唤醒;
- 被唤醒后,重新尝试获取锁(调用
L.Lock()); Wait()返回时,锁已被重新持有,goroutine 继续检查条件。
标准使用模式(必须在 for 循环中检查条件,不能使用 if):
mu.Lock()
for !condition() {cond.Wait()
}
// 此时条件满足,并持有锁
// 临界区代码 ...
mu.Unlock()
为什么必须用 for 而不是 if?
- 可能发生虚假唤醒(即使没有 Signal),Go 虽暂无此情况,但符合 POSIX 传统;
- 被唤醒后,其他 goroutine 可能抢在自身之前进入临界区并改变了条件;
- 用
for重新检查条件可保证逻辑正确。
2. Signal() —— 唤醒一个等待的 goroutine
唤醒等待队列中的一个 goroutine(如果有)。
调用 Signal 时不需要持有锁(但通常建议在持有锁的临界区内调用,以避免丢失唤醒)。
典型场景:生产者生产一个数据后,通知一个消费者。
3. Broadcast() —— 唤醒所有等待的 goroutine
唤醒等待队列中的所有 goroutine。
同样调用时不要求持有锁,但一般在修改了可能导致很多等待者条件成立的共享状态后调用。
典型场景:全局状态变更,所有等待者的条件都需要重新评估(比如退出信号、缓存被清空等)。
四、内部实现原理(简明版)
sync.Cond 基于运行时内部的 notifyList 实现,本质上是一个信号量 + 等待队列:
notifyList保存 goroutine 链表,包含wait(等待计数器)和notify(通知计数器)。Wait()增加wait,将 goroutine 挂起(gopark),等待通知。Signal()增加notify,唤醒队首 goroutine(如果notify < wait)。Broadcast()将notify设为wait,唤醒全部。
释放锁和挂起是原子地完成(防止错过通知),内部使用原子操作和运行时调度器协作。
自旋和饥饿模式不涉及 Cond,因为 Cond 直接使用的是锁和底层的 semaphore 式等待。
五、典型用法示例
示例 1:生产者-消费者队列(固定容量)
package mainimport ("fmt""sync"
)type Queue struct {mu sync.Mutexitems []intcap intcond *sync.Cond
}func NewQueue(capacity int) *Queue {q := &Queue{cap: capacity}q.cond = sync.NewCond(&q.mu)return q
}func (q *Queue) Put(item int) {q.mu.Lock()defer q.mu.Unlock()for len(q.items) == q.cap { // 队列满则等待q.cond.Wait()}q.items = append(q.items, item)q.cond.Signal() // 通知一个等待的取操作
}func (q *Queue) Take() int {q.mu.Lock()defer q.mu.Unlock()for len(q.items) == 0 { // 队列空则等待q.cond.Wait()}item := q.items[0]q.items = q.items[1:]q.cond.Signal() // 通知一个等待的放操作return item
}
示例 2:等待某个条件一次性满足(如初始化完成)
var (initialized boolmu sync.Mutexcond = sync.NewCond(&mu)
)func initResource() {mu.Lock()// 初始化...initialized = truemu.Unlock()cond.Broadcast() // 通知所有等待者
}func useResource() {mu.Lock()for !initialized {cond.Wait()}// 使用资源...mu.Unlock()
}
注意,这里 Broadcast 放在 Unlock 之后调用也可以,但在 Unlock 之前调用通常更安全,避免某些竞态;两种都可以,根据语义选择。
六、常见陷坑与最佳实践
| 要点 | 说明 |
|---|---|
| 必须持有锁再 Wait | Go 会检查并可能 panic,忘了会导致数据竞争或死锁 |
| Wait 必须在循环里 | 用 for 而非 if,防止虚假唤醒和条件变化 |
| 不要复制 Cond | sync.Cond 含 noCopy,传递只能用指针 |
| Signal/Broadcast 并非必须加锁 | 调用可无锁,但为清晰通常放在临界区内,避免错过唤醒 |
| 与 Channel 的抉择 | Channel 更简单直接;但当需要反复检查复杂条件且互斥操作多时,Cond 更灵活 |
与 sync.Once 区分 |
Once 是一次性初始化;Cond 用于可反复变化的条件等待 |
七、总结
sync.Cond是 Go 的标准条件变量实现,与互斥锁配合实现 “等待条件成立” 的同步模型。- Wait 原子解锁、挂起,被唤醒后重新加锁返回,必须在循环中使用。
- Signal 唤醒一个,Broadcast 唤醒全部。
- 不可复制,使用指针传递。
- 适合需要基于复杂条件协调 goroutine 的场景,如资源池、有界队列、状态依赖的执行等。
