Cond条件变量实现机制
1. 引言:条件变量的本质
同步原语的局限性分析
在并发编程中,我们经常遇到需要等待某个条件成立的场景。虽然Go语言推崇"Don't communicate by sharing memory; share memory by communicating"的理念,但在某些复杂同步场景下,传统的同步原语如Mutex、Channel等可能不够灵活:
- Mutex:只能实现互斥访问,无法表达"等待条件"的语义
- Channel:虽然强大,但在复杂的多条件等待场景下可能过于重量级
- 简单轮询:会消耗大量CPU资源,效率低下
条件变量解决的问题场景
条件变量专门为解决"等待条件成立"的同步问题而设计:
// 经典的生产者-消费者问题
// 消费者需要等待队列非空
// 生产者需要等待队列未满
// 使用条件变量的标准模式
mutex.Lock()
for !condition() {
cond.Wait() // 等待条件成立
}
// 条件已成立,执行相关操作
doSomething()
mutex.Unlock()
Go 语言中 Cond 的设计定位
Go的sync.Cond为条件等待提供了轻量级的解决方案,它具有以下特点:
- 与锁深度集成:必须与Locker配合使用
- 原子操作优化:基于ticket系统实现高效的等待/通知机制
- 运行时协作:直接与Go调度器交互,实现零开销的Goroutine挂起/唤醒
2. Cond 核心结构解析
sync.Cond 公共接口结构
让我们首先分析sync.Cond的核心结构:
// go/src/sync/cond.go
type Cond struct {
noCopy noCopy // 防止结构体被拷贝
// L是观察或改变条件时必须持有的锁
// 通常是*Mutex或*RWMutex
L Locker
// notify是底层的通知列表,实现真正的等待/通知逻辑
notify notifyList
// checker用于检测Cond是否被非法拷贝
checker copyChecker
}
设计要点解析:
- noCopy字段:确保Cond实例不能被拷贝,因为拷贝会破坏内部状态的一致性
- Locker接口:与具体的锁实现解耦,支持Mutex和RWMutex
- notifyList:核心的等待/通知机制,隐藏复杂的实现细节
- copyChecker:运行时检查,防止拷贝导致的竞态条件
notifyList 私有实现(runtime 层)
notifyList是Cond的核心,实现在runtime包中:
// go/src/runtime/sema.go
type notifyList struct {
// wait是下一个等待者的票号,在锁外原子递增
// 这是一个关键的无锁计数器
wait atomic.Uint32
// notify是下一个要被通知的等待者的票号
// 可以在锁外读取,但只能在持有锁时写入
//
// wait和notify都可能溢出32位,但只要它们的
// "展开"差值被2^31限制,就能正确处理溢出情况
// 这要求不能有超过2^31个goroutine同时阻塞在同一个条件变量上
notify uint32
// 已停放等待者的链表
lock mutex // 保护head和tail的互斥锁
head *sudog // 等待队列头指针
tail *sudog // 等待队列尾指针
}
关键设计机制:
- Ticket系统:使用原子递增的票号避免ABA问题
- 双计数器模型:wait计数新等待者,notify跟踪已通知数量
- 链表队列:使用sudog构建等待队列,与Go调度器深度集成
3. 核心操作流程分析
Wait() 操作路径
Wait()操作是条件变量的核心,其实现体现了精妙的同步设计:
// go/src/sync/cond.go
func (c *Cond) Wait() {
// 1. 检查Cond是否被拷贝(防御性编程)
c.checker.check()
// 2. 获取等待票号(原子操作,无锁)
// 这一步是关键:在释放锁之前就获得了等待的"身份证"
t := runtime_notifyListAdd(&c.notify)
// 3. 释放用户锁,允许其他goroutine修改条件
c.L.Unlock()
// 4. 等待通知(可能挂起当前goroutine)
runtime_notifyListWait(&c.notify, t)
// 5. 被唤醒后重新获取锁
c.L.Lock()
}
让我们深入分析runtime层的实现:
// go/src/runtime/sema.go
func notifyListAdd(l *notifyList) uint32 {
// 原子递增wait计数器,返回当前goroutine的票号
// 减1是因为Add返回递增后的值,而我们需要递增前的值作为票号
return l.wait.Add(1) - 1
}
等待计数原子操作的精妙之处:
这个看似简单的原子操作解决了一个复杂的竞态条件问题:
- 如果先释放锁再获取票号,可能错过通知
- 如果在锁内获取票号,会影响并发性能
- 原子操作确保了获取票号的过程不会被中断
Wait()操作详细流程图:
graph TD
A[开始: c.Wait调用] --> B[checker.check检查拷贝]
B --> C[t = runtime_notifyListAdd获取票号]
C --> D[原子操作: wait.Add1-1]
D --> E[c.L.Unlock释放用户锁]
E --> F[runtime_notifyListWait等待通知]
F --> G[lockWithRank获取内部锁]
G --> H{less t,l.notify?}
H -->|是| I[unlock释放内部锁]
I --> J[立即返回快速路径]
H -->|否| K[acquireSudog获取等待描述符]
K --> L[设置sudog字段: g,ticket,releasetime]
L --> M{l.tail == nil?}
M -->|是| N[l.head = s 设置队列头]
M -->|否| O[l.tail.next = s 追加到尾部]
N --> P[l.tail = s 设置队列尾]
O --> P
P --> Q[goparkunlock原子释放锁并挂起goroutine]
Q --> R[goroutine被挂起等待唤醒]
R --> S[被Signal/Broadcast唤醒]
S --> T[releaseSudog释放等待描述符]
T --> U[c.L.Lock重新获取用户锁]
U --> V[Wait返回]
J --> U
style A fill:#e1f5fe
style V fill:#c8e6c9
style Q fill:#ffccbc
style S fill:#fff3e0
// go/src/runtime/sema.go
func notifyListWait(l *notifyList, t uint32) {
// 获取notifyList的内部锁
lockWithRank(&l.lock, lockRankNotifyList)
// 如果这个票号已经被通知了,立即返回
// less函数处理uint32溢出情况 return int32(a-b) < 0
if less(t, l.notify) {
unlock(&l.lock)
return
}
// 需要真正挂起等待
// 1. 获取sudog(goroutine的等待描述符)
s := acquireSudog()
s.g = getg() // 当前goroutine
s.ticket = t // 等待票号
s.releasetime = 0
// 2. 性能分析支持
t0 := int64(0)
if blockprofilerate > 0 {
t0 = cputicks()
s.releasetime = -1
}
// 3. 加入等待队列(FIFO顺序)
if l.tail == nil {
l.head = s
} else {
l.tail.next = s
}
l.tail = s
// 4. 挂起goroutine,释放锁
// goparkunlock是关键函数:原子地释放锁并挂起goroutine
goparkunlock(&l.lock, waitReasonSyncCondWait, traceBlockCondWait, 3)
// 5. 被唤醒后的清理工作
if t0 != 0 {
blockevent(s.releasetime-t0, 2)
}
releaseSudog(s)
}
挂起机制的关键要点:
- 原子挂起:
goparkunlock确保锁释放和goroutine挂起是原子的 - 票号检查:避免虚假唤醒,确保通知的准确性
- 队列管理:FIFO顺序保证公平性
Signal() 唤醒机制
单节点唤醒是条件变量的基本操作:
// go/src/sync/cond.go
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
runtime层的实现更加复杂:
// go/src/runtime/sema.go
func notifyListNotifyOne(l *notifyList) {
// 如果没有新的等待者,直接返回
if l.wait.Load() == atomic.Load(&l.notify) {
return
}
lockWithRank(&l.lock, lockRankNotifyList)
// 双重检查:获取锁后再次确认是否需要通知
t := l.notify
if t == l.wait.Load() {
unlock(&l.lock)
return
}
// 更新下一个通知票号
atomic.Store(&l.notify, t+1)
// 关键算法:在等待队列中寻找目标goroutine
// 这个扫描看起来是线性的,但实际上几乎总是很快停止
// 因为goroutine排队和取号是分开的,可能会有轻微的重排序
// 但我们期待要找的goroutine就在队列前面附近
for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
if s.ticket == t {
// 找到目标goroutine,从队列中移除
n := s.next
if p != nil {
p.next = n
} else {
l.head = n
}
if n == nil {
l.tail = p
}
unlock(&l.lock)
// 清理并唤醒goroutine
s.next = nil
readyWithTime(s, 4)
return
}
}
unlock(&l.lock)
}
单节点唤醒算法的优化设计:
- 双重检查模式:减少不必要的锁争用
- 线性搜索优化:利用FIFO特性,目标通常在队列前部
- 原子通知计数:避免重复通知
Signal()操作详细流程图:
graph TD
A[开始: c.Signal调用] --> B[checker.check检查拷贝]
B --> C[runtime_notifyListNotifyOne]
C --> D{l.wait.Load == l.notify?}
D -->|是| E[return 快速路径返回]
D -->|否| F[lockWithRank获取内部锁]
F --> G[t = l.notify 获取当前通知计数]
G --> H{t == l.wait.Load?}
H -->|是| I[unlock释放锁]
I --> J[return 双重检查返回]
H -->|否| K[atomic.Store l.notify, t+1]
K --> L[p = nil, s = l.head 开始遍历队列]
L --> M{s != nil?}
M -->|否| N[unlock释放锁]
N --> O[return 未找到目标]
M -->|是| P{s.ticket == t?}
P -->|否| Q[p = s, s = s.next 继续遍历]
Q --> M
P -->|是| R[找到目标sudog]
R --> S[n = s.next]
S --> T{p != nil?}
T -->|是| U[p.next = n 从中间移除]
T -->|否| V[l.head = n 从头部移除]
U --> W{n == nil?}
V --> W
W -->|是| X[l.tail = p 更新尾指针]
W -->|否| Y[unlock释放内部锁]
X --> Y
Y --> Z[s.next = nil 清理节点]
Z --> AA[readyWithTime唤醒goroutine]
AA --> BB[return 操作完成]
style A fill:#e1f5fe
style E fill:#c8e6c9
style J fill:#c8e6c9
style O fill:#c8e6c9
style BB fill:#c8e6c9
style AA fill:#fff3e0
Broadcast() 全唤醒机制
群体通知需要更复杂的队列批量处理:
// go/src/sync/cond.go
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
// go/src/runtime/sema.go
func notifyListNotifyAll(l *notifyList) {
// 快速路径检查
if l.wait.Load() == atomic.Load(&l.notify) {
return
}
// 关键优化:将整个等待队列移到局部变量
// 这样可以在锁外进行耗时的goroutine唤醒操作
lockWithRank(&l.lock, lockRankNotifyList)
s := l.head
l.head = nil
l.tail = nil
// 更新通知计数到当前等待计数
// 这样可以保证所有之前的等待者都会被通知
atomic.Store(&l.notify, l.wait.Load())
unlock(&l.lock)
// 批量唤醒所有等待者(锁外操作)
for s != nil {
next := s.next
s.next = nil
readyWithTime(s, 4) // 唤醒goroutine
s = next
}
}
全唤醒机制的性能优化:
- 队列迁移:避免长时间持锁
- 批量处理:一次性更新所有状态
- 锁外唤醒:减少锁争用,提高并发性
Broadcast()操作详细流程图:
graph TD
A[开始: c.Broadcast调用] --> B[checker.check检查拷贝]
B --> C[runtime_notifyListNotifyAll]
C --> D{l.wait.Load == l.notify?}
D -->|是| E[return 快速路径返回]
D -->|否| F[lockWithRank获取内部锁]
F --> G[s = l.head 保存队列头]
G --> H[l.head = nil 清空队列头]
H --> I[l.tail = nil 清空队列尾]
I --> J[atomic.Store l.notify, l.wait.Load]
J --> K[unlock释放内部锁]
K --> L{s != nil?}
L -->|否| M[return 无等待者返回]
L -->|是| N[next = s.next 保存下一个节点]
N --> O[s.next = nil 清理当前节点链接]
O --> P[readyWithTime唤醒当前goroutine]
P --> Q[s = next 移动到下一个节点]
Q --> R{s != nil?}
R -->|是| N
R -->|否| S[return 所有goroutine已唤醒]
style A fill:#e1f5fe
style E fill:#c8e6c9
style M fill:#c8e6c9
style S fill:#c8e6c9
style P fill:#fff3e0
style J fill:#ffccbc
4. 完整数据状态模拟
为了更好地理解Cond的工作机制,我们通过一个完整的数据状态模拟来展示Wait、Signal、Broadcast操作对内部数据结构的影响。
4.1 初始状态
notifyList状态:
┌──────────────────────────────────┐
│ wait: 0 (原子计数器) │
│ notify: 0 (通知计数器) │
│ head: nil (队列头指针) │
│ tail: nil (队列尾指针) │
└──────────────────────────────────┘
等待队列: 空
4.2 Goroutine A 调用 Wait()
操作前状态:
wait=0, notify=0, head=nil, tail=nil
队列: 空
操作步骤:
checker.check()- 检查拷贝t = notifyListAdd()- 原子操作wait.Add(1)-1, 获取ticket=0c.L.Unlock()- 释放用户锁notifyListWait(ticket=0)- 进入等待逻辑- 检查
less(0, 0)- false,需要真正等待 acquireSudog()- 获取等待描述符- 设置sudog:
g=goroutineA, ticket=0 l.tail == nil- 是,设置l.head = sudogA- 设置
l.tail = sudogA goparkunlock()- 挂起goroutine
操作后状态:
wait=1, notify=0, head=sudogA, tail=sudogA
队列: [sudogA(ticket=0,g=A)] -> nil
graph LR
subgraph "notifyList"
W1[wait: 1]
N1[notify: 0]
H1[head] --> SA[sudogA<br/>ticket=0<br/>g=A]
T1[tail] --> SA
SA --> NIL1[nil]
end
4.3 Goroutine B 调用 Wait()
操作前状态:
wait=1, notify=0, head=sudogA, tail=sudogA
队列: [sudogA(ticket=0)] -> nil
操作步骤:
t = notifyListAdd()- 原子操作,获取ticket=1c.L.Unlock()- 释放用户锁notifyListWait(ticket=1)- 进入等待逻辑- 检查
less(1, 0)- false,需要等待 - 获取并设置sudogB:
g=goroutineB, ticket=1 l.tail != nil- 否,执行l.tail.next = sudogB- 更新
l.tail = sudogB - 挂起goroutineB
操作后状态:
wait=2, notify=0, head=sudogA, tail=sudogB
队列: [sudogA(ticket=0)] -> [sudogB(ticket=1)] -> nil
graph LR
subgraph "notifyList"
W2[wait: 2]
N2[notify: 0]
H2[head] --> SA2[sudogA<br/>ticket=0<br/>g=A]
T2[tail] --> SB2[sudogB<br/>ticket=1<br/>g=B]
SA2 --> SB2
SB2 --> NIL2[nil]
end
4.4 Goroutine C 调用 Wait()
操作前状态:
wait=2, notify=0, head=sudogA, tail=sudogB
队列: [sudogA(ticket=0)] -> [sudogB(ticket=1)] -> nil
操作步骤:
t = notifyListAdd()- 获取ticket=2- 释放用户锁并进入等待
- 设置sudogC:
g=goroutineC, ticket=2 - 追加到队列尾部:
sudogB.next = sudogC - 更新
l.tail = sudogC - 挂起goroutineC
操作后状态:
wait=3, notify=0, head=sudogA, tail=sudogC
队列: [sudogA(ticket=0)] -> [sudogB(ticket=1)] -> [sudogC(ticket=2)] -> nil
graph LR
subgraph "notifyList"
W3[wait: 3]
N3[notify: 0]
H3[head] --> SA3[sudogA<br/>ticket=0<br/>g=A]
T3[tail] --> SC3[sudogC<br/>ticket=2<br/>g=C]
SA3 --> SB3[sudogB<br/>ticket=1<br/>g=B]
SB3 --> SC3
SC3 --> NIL3[nil]
end
4.5 调用 Signal() - 唤醒一个等待者
操作前状态:
wait=3, notify=0, head=sudogA, tail=sudogC
队列: [sudogA(ticket=0)] -> [sudogB(ticket=1)] -> [sudogC(ticket=2)] -> nil
操作步骤:
checker.check()- 检查拷贝- 快速路径检查:
wait(3) != notify(0)- 需要通知 - 获取内部锁
- 双重检查:
notify(0) != wait(3)- 确实需要通知 notify++- 更新为1- 遍历队列寻找
ticket=0的sudog - 找到sudogA,从队列头移除:
l.head = sudogA.next - 释放内部锁
readyWithTime(sudogA)- 唤醒goroutineA
操作后状态:
wait=3, notify=1, head=sudogB, tail=sudogC
队列: [sudogB(ticket=1)] -> [sudogC(ticket=2)] -> nil
goroutineA被唤醒
graph LR
subgraph "notifyList"
W4[wait: 3]
N4[notify: 1]
H4[head] --> SB4[sudogB<br/>ticket=1<br/>g=B]
T4[tail] --> SC4[sudogC<br/>ticket=2<br/>g=C]
SB4 --> SC4
SC4 --> NIL4[nil]
end
subgraph "已唤醒"
SA4[sudogA<br/>ticket=0<br/>g=A<br/>AWAKENED]
end
4.6 再次调用 Signal() - 唤醒第二个等待者
操作前状态:
wait=3, notify=1, head=sudogB, tail=sudogC
队列: [sudogB(ticket=1)] -> [sudogC(ticket=2)] -> nil
操作步骤:
- 快速路径检查失败,获取内部锁
notify++- 更新为2- 遍历队列寻找
ticket=1的sudog - 找到sudogB,从队列头移除:
l.head = sudogB.next - 唤醒goroutineB
操作后状态:
wait=3, notify=2, head=sudogC, tail=sudogC
队列: [sudogC(ticket=2)] -> nil
goroutineA,B已唤醒
graph LR
subgraph "notifyList"
W5[wait: 3]
N5[notify: 2]
H5[head] --> SC5[sudogC<br/>ticket=2<br/>g=C]
T5[tail] --> SC5
SC5 --> NIL5[nil]
end
subgraph "已唤醒"
SA5[sudogA<br/>AWAKENED]
SB5[sudogB<br/>AWAKENED]
end
4.7 调用 Broadcast() - 唤醒所有剩余等待者
操作前状态:
wait=3, notify=2, head=sudogC, tail=sudogC
队列: [sudogC(ticket=2)] -> nil
操作步骤:
checker.check()- 检查拷贝- 快速路径检查失败,获取内部锁
s = l.head- 保存队列头sudogCl.head = nil, l.tail = nil- 清空队列notify = wait- 批量更新为3- 释放内部锁
- 遍历本地队列,逐个唤醒:
readyWithTime(sudogC)- 唤醒goroutineC
操作后状态:
wait=3, notify=3, head=nil, tail=nil
队列: 空
所有goroutine已唤醒
graph LR
subgraph "notifyList"
W6[wait: 3]
N6[notify: 3]
H6[head: nil]
T6[tail: nil]
end
subgraph "已唤醒"
SA6[sudogA<br/>AWAKENED]
SB6[sudogB<br/>AWAKENED]
SC6[sudogC<br/>AWAKENED]
end
4.8 新的Goroutine D 调用 Wait()
操作前状态:
wait=3, notify=3, head=nil, tail=nil
队列: 空
操作步骤:
t = notifyListAdd()- 获取ticket=3- 进入
notifyListWait(ticket=3) - 检查
less(3, 3)- false,需要等待 - 创建sudogD并加入队列
操作后状态:
wait=4, notify=3, head=sudogD, tail=sudogD
队列: [sudogD(ticket=3)] -> nil
4.9 数据状态变化总结
graph TB
subgraph "完整操作流程"
A1[初始: wait=0,notify=0] --> A2[Wait A: wait=1,notify=0]
A2 --> A3[Wait B: wait=2,notify=0]
A3 --> A4[Wait C: wait=3,notify=0]
A4 --> A5[Signal: wait=3,notify=1]
A5 --> A6[Signal: wait=3,notify=2]
A6 --> A7[Broadcast: wait=3,notify=3]
A7 --> A8[Wait D: wait=4,notify=3]
end
style A1 fill:#e3f2fd
style A7 fill:#c8e6c9
这个完整的模拟展示了:
- 票号系统的FIFO特性:ticket按获取顺序分配,按顺序唤醒
- 原子计数器的作用:wait跟踪总等待者,notify跟踪已通知数量
- 队列管理的精确性:head/tail指针的正确维护
- Signal vs Broadcast的区别:Signal逐个唤醒,Broadcast批量清空
- 快速路径优化:
wait == notify时无需复杂操作
通过这些详细的状态变化,我们可以清楚地看到Go的Cond实现是如何在保证正确性的同时实现高性能的。
5. Ticket溢出处理机制
5.1 32位溢出问题
在长期运行的系统中,ticket计数器可能会溢出32位整数范围。Go通过巧妙的设计处理了这个问题:
// go/src/runtime/sema.go
// less检查a < b,考虑到a和b可能溢出32位范围的运行计数
// 它们的"展开"差值总是小于2^31
func less(a, b uint32) bool {
return int32(a-b) < 0
}
5.1 溢出边界条件分析
核心约束条件:
wait和notify的差值必须 < 2^31 (约21亿)
这意味着系统中同时等待的goroutine数量不能超过21亿个,这在实际应用中是完全可行的约束。
graph TD
A[ticket计数器设计] --> B[32位无符号整数]
B --> C[溢出循环特性]
C --> D[差值计算法]
D --> E[int32符号判断]
E --> F[支持21亿并发等待者]
G[关键约束] --> H[wait-notify < 2^31]
H --> I[实际场景完全满足]
J[边界测试] --> K[接近溢出: 4294967290]
K --> L[跨越溢出: 0,1,2...]
L --> M[继续正常工作]
style A fill:#e1f5fe
style F fill:#c8e6c9
style I fill:#c8e6c9
style M fill:#c8e6c9
5.3 溢出场景完整测试
// 模拟极端溢出场景的测试
func testTicketOverflow() {
// 模拟接近uint32最大值的情况
// 步骤1: 系统运行很长时间,ticket接近溢出
wait := uint32(4294967290) // 距离溢出还有5个
notify := uint32(4294967288) // 已通知到这里
// 现在有2个等待者: ticket=4294967288, ticket=4294967289
// 步骤2: 继续添加等待者,触发溢出
wait = wait + 10 // 溢出后变成 4 (4294967300 % 2^32)
// 步骤3: 现在的状态
// wait = 4 (实际代表第4294967300个等待者)
// notify = 4294967288
// 队列中有票号: 4294967288, 4294967289, 4294967290, ..., 4294967299, 0, 1, 2, 3
// 步骤4: 调用Signal()寻找ticket=4294967288
targetTicket := notify
currentTicket := uint32(4294967288)
found := targetTicket == currentTicket // true,找到目标
// 步骤5: 继续Signal(),处理溢出边界
notify = 4294967299 // 下一个要通知的票号
wait = 4 // 当前等待计数
// less(4294967299, 4) 的计算:
// int32(4294967299 - 4) = int32(4294967295) = -1 < 0 = true
// 说明4294967299确实小于4,需要通知
fmt.Printf("溢出处理正确:less(4294967299, 4) = %v\n",
less(4294967299, 4)) // true
}
6. 操作用例集合
6.1 基础使用模式
6.1.1 经典生产者-消费者模式
package main
import (
"fmt"
"sync"
"time"
)
// 线程安全的环形缓冲区
type RingBuffer struct {
mu sync.Mutex
notEmpty *sync.Cond // 消费者等待非空
notFull *sync.Cond // 生产者等待非满
buffer []interface{}
size int
head int // 读取位置
tail int // 写入位置
count int // 当前元素数量
}
func NewRingBuffer(size int) *RingBuffer {
rb := &RingBuffer{
buffer: make([]interface{}, size),
size: size,
}
rb.notEmpty = sync.NewCond(&rb.mu)
rb.notFull = sync.NewCond(&rb.mu)
return rb
}
func (rb *RingBuffer) Put(item interface{}) {
rb.mu.Lock()
defer rb.mu.Unlock()
// 等待缓冲区有空间
for rb.count == rb.size {
fmt.Printf("生产者等待:缓冲区已满 (count=%d)\n", rb.count)
rb.notFull.Wait()
}
// 放入元素
rb.buffer[rb.tail] = item
rb.tail = (rb.tail + 1) % rb.size
rb.count++
fmt.Printf("生产:%v (count=%d)\n", item, rb.count)
// 通知消费者
rb.notEmpty.Signal()
}
func (rb *RingBuffer) Get() interface{} {
rb.mu.Lock()
defer rb.mu.Unlock()
// 等待缓冲区非空
for rb.count == 0 {
fmt.Printf("消费者等待:缓冲区为空\n")
rb.notEmpty.Wait()
}
// 取出元素
item := rb.buffer[rb.head]
rb.buffer[rb.head] = nil // 清理引用
rb.head = (rb.head + 1) % rb.size
rb.count--
fmt.Printf("消费:%v (count=%d)\n", item, rb.count)
// 通知生产者
rb.notFull.Signal()
return item
}
func main() {
buffer := NewRingBuffer(3)
// 启动多个生产者
for i := 0; i < 2; i++ {
go func(id int) {
for j := 0; j < 5; j++ {
item := fmt.Sprintf("Producer%d-Item%d", id, j)
buffer.Put(item)
time.Sleep(100 * time.Millisecond)
}
}(i)
}
// 启动多个消费者
for i := 0; i < 2; i++ {
go func(id int) {
for j := 0; j < 5; j++ {
item := buffer.Get()
fmt.Printf("Consumer%d 获得: %v\n", id, item)
time.Sleep(150 * time.Millisecond)
}
}(i)
}
time.Sleep(3 * time.Second)
}
6.1.2 工作池模式
// 使用Cond实现的工作池
type WorkerPool struct {
mu sync.Mutex
cond *sync.Cond
tasks []func()
workers int
shutdown bool
}
func NewWorkerPool(numWorkers int) *WorkerPool {
wp := &WorkerPool{
workers: numWorkers,
tasks: make([]func(), 0),
}
wp.cond = sync.NewCond(&wp.mu)
// 启动工作者
for i := 0; i < numWorkers; i++ {
go wp.worker(i)
}
return wp
}
func (wp *WorkerPool) worker(id int) {
for {
wp.mu.Lock()
// 等待任务或关闭信号
for len(wp.tasks) == 0 && !wp.shutdown {
fmt.Printf("Worker%d 等待任务\n", id)
wp.cond.Wait()
}
// 检查是否需要关闭
if wp.shutdown && len(wp.tasks) == 0 {
wp.mu.Unlock()
fmt.Printf("Worker%d 退出\n", id)
return
}
// 获取任务
task := wp.tasks[0]
wp.tasks = wp.tasks[1:]
wp.mu.Unlock()
// 执行任务
fmt.Printf("Worker%d 执行任务\n", id)
task()
}
}
func (wp *WorkerPool) Submit(task func()) {
wp.mu.Lock()
defer wp.mu.Unlock()
if wp.shutdown {
panic("工作池已关闭")
}
wp.tasks = append(wp.tasks, task)
wp.cond.Signal() // 唤醒一个工作者
}
func (wp *WorkerPool) Shutdown() {
wp.mu.Lock()
defer wp.mu.Unlock()
wp.shutdown = true
wp.cond.Broadcast() // 唤醒所有工作者
}
6.2 高级使用模式
6.2.1 读写者问题
// 使用Cond实现读写锁
type ReadWriteLock struct {
mu sync.Mutex
readersCond *sync.Cond
writersCond *sync.Cond
readers int // 当前读者数量
writer bool // 是否有写者
waitingWriters int // 等待的写者数量
}
func NewReadWriteLock() *ReadWriteLock {
rwl := &ReadWriteLock{}
rwl.readersCond = sync.NewCond(&rwl.mu)
rwl.writersCond = sync.NewCond(&rwl.mu)
return rwl
}
func (rwl *ReadWriteLock) RLock() {
rwl.mu.Lock()
defer rwl.mu.Unlock()
// 如果有写者或等待的写者,读者需要等待
for rwl.writer || rwl.waitingWriters > 0 {
rwl.readersCond.Wait()
}
rwl.readers++
fmt.Printf("读者获得锁,当前读者数:%d\n", rwl.readers)
}
func (rwl *ReadWriteLock) RUnlock() {
rwl.mu.Lock()
defer rwl.mu.Unlock()
rwl.readers--
fmt.Printf("读者释放锁,当前读者数:%d\n", rwl.readers)
// 如果没有读者了,唤醒等待的写者
if rwl.readers == 0 {
rwl.writersCond.Signal()
}
}
func (rwl *ReadWriteLock) Lock() {
rwl.mu.Lock()
defer rwl.mu.Unlock()
rwl.waitingWriters++
// 等待没有读者和写者
for rwl.readers > 0 || rwl.writer {
rwl.writersCond.Wait()
}
rwl.waitingWriters--
rwl.writer = true
fmt.Printf("写者获得锁\n")
}
func (rwl *ReadWriteLock) Unlock() {
rwl.mu.Lock()
defer rwl.mu.Unlock()
if !rwl.writer {
panic("没有写者持有锁")
}
rwl.writer = false
fmt.Printf("写者释放锁\n")
// 优先唤醒等待的写者,如果没有则唤醒所有读者
if rwl.waitingWriters > 0 {
rwl.writersCond.Signal()
} else {
rwl.readersCond.Broadcast()
}
}
6.2.2 屏障同步模式
// Barrier实现:等待所有goroutine到达同一点后继续
type Barrier struct {
mu sync.Mutex
cond *sync.Cond
n int // 需要等待的goroutine数量
count int // 已到达的goroutine数量
generation int // 当前代数,防止重用问题
}
func NewBarrier(n int) *Barrier {
b := &Barrier{n: n}
b.cond = sync.NewCond(&b.mu)
return b
}
func (b *Barrier) Wait() {
b.mu.Lock()
defer b.mu.Unlock()
// 记录当前代数,防止被后续Wait调用影响
gen := b.generation
b.count++
if b.count == b.n {
// 最后一个到达的goroutine
fmt.Printf("所有goroutine已到达屏障,释放所有等待者\n")
b.count = 0
b.generation++
b.cond.Broadcast()
} else {
// 等待其他goroutine
for b.count < b.n && gen == b.generation {
fmt.Printf("等待其他goroutine到达屏障 (%d/%d)\n", b.count, b.n)
b.cond.Wait()
}
}
}
// 使用示例
func barrierExample() {
const numGoroutines = 5
barrier := NewBarrier(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(id int) {
// 第一阶段工作
workTime := time.Duration(rand.Intn(1000)) * time.Millisecond
fmt.Printf("Goroutine%d 工作第一阶段 (%v)\n", id, workTime)
time.Sleep(workTime)
fmt.Printf("Goroutine%d 到达屏障\n", id)
barrier.Wait()
// 第二阶段工作
fmt.Printf("Goroutine%d 开始第二阶段工作\n", id)
time.Sleep(500 * time.Millisecond)
fmt.Printf("Goroutine%d 完成所有工作\n", id)
}(i)
}
time.Sleep(5 * time.Second)
}
6.3 错误使用案例分析
6.3.1 常见错误模式
// ❌ 错误1:忘记在循环中检查条件
func wrongPattern1() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
ready := false
go func() {
mu.Lock()
defer mu.Unlock()
if !ready { // 错误:应该使用for循环
cond.Wait()
}
// 这里可能ready仍然为false!
doWork()
}()
// 可能的问题:虚假唤醒导致条件未满足就继续执行
}
// ✅ 正确1:使用for循环
func correctPattern1() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
ready := false
go func() {
mu.Lock()
defer mu.Unlock()
for !ready { // 正确:循环检查条件
cond.Wait()
}
doWork()
}()
}
// ❌ 错误2:Signal/Broadcast时持有锁时间过长
func wrongPattern2() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
mu.Lock()
defer mu.Unlock()
updateCondition()
cond.Signal() // 在锁内调用
doExpensiveWork() // 昂贵操作阻止被唤醒的goroutine获取锁
}
// ✅ 正确2:锁外调用Signal
func correctPattern2() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
mu.Lock()
updateCondition()
mu.Unlock()
cond.Signal() // 锁外调用
doExpensiveWork()
}
// ❌ 错误3:拷贝Cond结构体
func wrongPattern3() {
var mu sync.Mutex
cond1 := sync.NewCond(&mu)
cond2 := *cond1 // 错误:拷贝了Cond
// cond2.Wait() // 会panic: sync.Cond is copied
}
// ❌ 错误4:不正确的Broadcast使用
func wrongPattern4() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
resource := 0
// 错误:不必要的Broadcast
go func() {
mu.Lock()
resource = 1 // 只增加了一个资源
mu.Unlock()
cond.Broadcast() // 但唤醒了所有等待者,可能导致惊群
}()
}
// ✅ 正确4:合理使用Signal vs Broadcast
func correctPattern4() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
resource := 0
// 正确:根据场景选择
go func() {
mu.Lock()
resource++ // 增加一个资源
mu.Unlock()
cond.Signal() // 只唤醒一个等待者
}()
// Broadcast适用场景:
go func() {
mu.Lock()
shutdown = true // 全局状态变更
mu.Unlock()
cond.Broadcast() // 需要通知所有等待者
}()
}
6.3.2 性能陷阱
// 性能陷阱:不必要的唤醒
type BadQueue struct {
mu sync.Mutex
cond *sync.Cond
items []int
}
func (q *BadQueue) BadEnqueue(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.cond.Broadcast() // 错误:总是Broadcast
}
func (q *BadQueue) BadDequeue() 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.Broadcast() // 错误:总是Broadcast
return item
}
// 优化版本
type GoodQueue struct {
mu sync.Mutex
notEmpty *sync.Cond
notFull *sync.Cond
items []int
maxSize int
}
func (q *GoodQueue) Enqueue(item int) {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) >= q.maxSize {
q.notFull.Wait()
}
q.items = append(q.items, item)
q.notEmpty.Signal() // 只唤醒一个消费者
}
func (q *GoodQueue) Dequeue() int {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
q.notEmpty.Wait()
}
item := q.items[0]
q.items = q.items[1:]
q.notFull.Signal() // 只唤醒一个生产者
return item
}
6.4 与其他同步原语的对比
// 对比:Channel vs Cond
func compareChannelAndCond() {
// Channel方式:简洁但可能内存开销大
func channelBasedQueue() {
ch := make(chan int, 100) // 需要缓冲区
// 生产者
go func() {
for i := 0; i < 1000; i++ {
ch <- i // 阻塞时自动休眠
}
}()
// 消费者
go func() {
for item := range ch {
process(item)
}
}()
}
// Cond方式:更精细的控制
func condBasedQueue() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
queue := make([]int, 0)
// 生产者
go func() {
for i := 0; i < 1000; i++ {
mu.Lock()
for len(queue) >= 100 { // 精确控制队列大小
cond.Wait()
}
queue = append(queue, i)
cond.Signal()
mu.Unlock()
}
}()
// 消费者
go func() {
for {
mu.Lock()
for len(queue) == 0 {
cond.Wait()
}
item := queue[0]
queue = queue[1:]
cond.Signal()
mu.Unlock()
process(item)
}
}()
}
}
总结
条件变量作为经典的同步原语,在Go中的实现充分体现了现代编程语言的设计智慧。它不仅是一个实用的工具,更是理解并发编程本质的窗口。通过这个窗口,我们能够窥见Go语言设计者对性能、正确性、易用性的深度思考和精心平衡。
这种深度理解不仅有助于我们编写更好的Go代码,更重要的是培养了我们分析复杂系统、设计优雅解决方案的能力。这些能力将在我们面对更广泛的技术挑战时发挥重要作用,帮助我们成为更优秀的程序员和系统设计者。