Golang源码分析(十二) Cond条件变量实现机制

127 阅读17分钟

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为条件等待提供了轻量级的解决方案,它具有以下特点:

  1. 与锁深度集成:必须与Locker配合使用
  2. 原子操作优化:基于ticket系统实现高效的等待/通知机制
  3. 运行时协作:直接与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
}

设计要点解析:

  1. noCopy字段:确保Cond实例不能被拷贝,因为拷贝会破坏内部状态的一致性
  2. Locker接口:与具体的锁实现解耦,支持Mutex和RWMutex
  3. notifyList:核心的等待/通知机制,隐藏复杂的实现细节
  4. 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   // 等待队列尾指针
}

关键设计机制:

  1. Ticket系统:使用原子递增的票号避免ABA问题
  2. 双计数器模型:wait计数新等待者,notify跟踪已通知数量
  3. 链表队列:使用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)
}

挂起机制的关键要点:

  1. 原子挂起goparkunlock确保锁释放和goroutine挂起是原子的
  2. 票号检查:避免虚假唤醒,确保通知的准确性
  3. 队列管理: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)
}

单节点唤醒算法的优化设计:

  1. 双重检查模式:减少不必要的锁争用
  2. 线性搜索优化:利用FIFO特性,目标通常在队列前部
  3. 原子通知计数:避免重复通知

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
    }
}

全唤醒机制的性能优化:

  1. 队列迁移:避免长时间持锁
  2. 批量处理:一次性更新所有状态
  3. 锁外唤醒:减少锁争用,提高并发性

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
队列: 空

操作步骤:

  1. checker.check() - 检查拷贝
  2. t = notifyListAdd() - 原子操作 wait.Add(1)-1, 获取ticket=0
  3. c.L.Unlock() - 释放用户锁
  4. notifyListWait(ticket=0) - 进入等待逻辑
  5. 检查 less(0, 0) - false,需要真正等待
  6. acquireSudog() - 获取等待描述符
  7. 设置sudog: g=goroutineA, ticket=0
  8. l.tail == nil - 是,设置 l.head = sudogA
  9. 设置 l.tail = sudogA
  10. 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

操作步骤:

  1. t = notifyListAdd() - 原子操作,获取ticket=1
  2. c.L.Unlock() - 释放用户锁
  3. notifyListWait(ticket=1) - 进入等待逻辑
  4. 检查 less(1, 0) - false,需要等待
  5. 获取并设置sudogB: g=goroutineB, ticket=1
  6. l.tail != nil - 否,执行 l.tail.next = sudogB
  7. 更新 l.tail = sudogB
  8. 挂起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

操作步骤:

  1. t = notifyListAdd() - 获取ticket=2
  2. 释放用户锁并进入等待
  3. 设置sudogC: g=goroutineC, ticket=2
  4. 追加到队列尾部: sudogB.next = sudogC
  5. 更新 l.tail = sudogC
  6. 挂起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

操作步骤:

  1. checker.check() - 检查拷贝
  2. 快速路径检查: wait(3) != notify(0) - 需要通知
  3. 获取内部锁
  4. 双重检查: notify(0) != wait(3) - 确实需要通知
  5. notify++ - 更新为1
  6. 遍历队列寻找 ticket=0 的sudog
  7. 找到sudogA,从队列头移除: l.head = sudogA.next
  8. 释放内部锁
  9. 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

操作步骤:

  1. 快速路径检查失败,获取内部锁
  2. notify++ - 更新为2
  3. 遍历队列寻找 ticket=1 的sudog
  4. 找到sudogB,从队列头移除: l.head = sudogB.next
  5. 唤醒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

操作步骤:

  1. checker.check() - 检查拷贝
  2. 快速路径检查失败,获取内部锁
  3. s = l.head - 保存队列头sudogC
  4. l.head = nil, l.tail = nil - 清空队列
  5. notify = wait - 批量更新为3
  6. 释放内部锁
  7. 遍历本地队列,逐个唤醒:
    • 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
队列: 空

操作步骤:

  1. t = notifyListAdd() - 获取ticket=3
  2. 进入 notifyListWait(ticket=3)
  3. 检查 less(3, 3) - false,需要等待
  4. 创建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

这个完整的模拟展示了:

  1. 票号系统的FIFO特性:ticket按获取顺序分配,按顺序唤醒
  2. 原子计数器的作用:wait跟踪总等待者,notify跟踪已通知数量
  3. 队列管理的精确性:head/tail指针的正确维护
  4. Signal vs Broadcast的区别:Signal逐个唤醒,Broadcast批量清空
  5. 快速路径优化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代码,更重要的是培养了我们分析复杂系统、设计优雅解决方案的能力。这些能力将在我们面对更广泛的技术挑战时发挥重要作用,帮助我们成为更优秀的程序员和系统设计者。