Go语言条件变量sync.Cond:线程间的协调者

文章目录

书接上回:《Go语言中的互斥锁:sync.Mutex与sync.RWMutex》

在并发编程中,有时简单的互斥锁和读写锁是不够的。想象这样一个场景:一个goroutine需要等待某个条件成立才能继续执行,而另一个goroutine负责改变这个条件。这就是条件变量(sync.Cond)的用武之地。

条件变量提供了一种机制,让goroutine能够等待特定条件发生,并在条件发生时被通知唤醒。它像是goroutine之间的"信号系统",协调它们的执行顺序。

条件变量的基本概念

条件变量是基于互斥锁的同步工具,它允许goroutine在满足特定条件之前等待,而不是忙等待(busy-waiting)。忙等待会消耗CPU资源,而条件变量能让goroutine进入睡眠状态,直到被唤醒。

使用场景:当你需要让goroutine等待某个条件成立,而不是简单地等待一段时间时,条件变量是最佳选择。

注意事项

  • 条件变量必须与互斥锁配合使用
  • 条件变量的Wait方法会自动释放锁,唤醒时会重新获取锁
  • 总是使用for循环而不是if语句检查条件,防止虚假唤醒

下面是基本示例:

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

func basicCondExample() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    
    var ready bool
    
    // Goroutine 1: 等待条件成立
    go func() {
        fmt.Println("Goroutine 1: 等待条件")
        
        mu.Lock()
        for !ready {  // 使用for循环而不是if
            fmt.Println("Goroutine 1: 条件不满足,进入等待")
            cond.Wait() // 释放锁并阻塞
            fmt.Println("Goroutine 1: 被唤醒")
        }
        mu.Unlock()
        
        fmt.Println("Goroutine 1: 条件满足,继续执行")
    }()
    
    // Goroutine 2: 设置条件
    go func() {
        time.Sleep(2 * time.Second)
        
        mu.Lock()
        ready = true
        fmt.Println("Goroutine 2: 设置条件为true")
        mu.Unlock()
        
        cond.Signal() // 唤醒一个等待的goroutine
        fmt.Println("Goroutine 2: 发送信号")
    }()
    
    time.Sleep(3 * time.Second)
}

func main() {
    fmt.Println("=== 条件变量基本示例 ===")
    basicCondExample()
}

https://gitee.com/rxbook/go-demo-2025/blob/master/demo/concurrent2/demo02_cond/main1.go

复制代码
go run demo02_cond/main1.go 
=== 条件变量基本示例 ===
Goroutine 1: 等待条件
Goroutine 1: 条件不满足,进入等待
Goroutine 2: 设置条件为true
Goroutine 2: 发送信号
Goroutine 1: 被唤醒
Goroutine 1: 条件满足,继续执行

代码说明:

  • sync.NewCond(&mu) 创建条件变量,必须传入一个互斥锁
  • cond.Wait() 会自动释放锁并阻塞,被唤醒后会自动重新获取锁
  • 必须使用 for !ready 而不是 if !ready,防止虚假唤醒
  • cond.Signal() 唤醒一个等待的goroutine
  • 典型模式:获取锁 → 检查条件 → Wait(不满足)→ 执行操作 → 释放锁

条件变量的三个核心方法

条件变量提供了三个核心方法来协调goroutine之间的同步:
条件变量 cond Wait 等待 Signal 通知 Broadcast 广播 释放锁并阻塞 被唤醒后重新获取锁 唤醒一个等待的goroutine 唤醒所有等待的goroutine goroutine进入等待队列 检查条件是否满足 从等待队列中取出一个 清空等待队列

Wait()方法的工作流程:
否 是 检查条件 持有锁 进入等待队列 释放锁 阻塞等待 被Signal/Broadcast唤醒 重新获取锁 执行临界区操作 释放锁

条件变量的内部工作原理

Wait方法的详细过程

理解Wait()方法的内部机制对于正确使用条件变量至关重要。以下是Wait方法的执行步骤:

  1. 获取关联的互斥锁
  2. 检查条件是否满足
  3. 如果条件不满足,将当前goroutine加入等待队列
  4. 释放互斥锁并阻塞当前goroutine
  5. 被唤醒后重新获取互斥锁
  6. 再次检查条件(防止虚假唤醒)

下面是详细的代码演示:

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

type ConditionDetails struct {
    mu    sync.Mutex
    cond  *sync.Cond
    value int
}

func (cd *ConditionDetails) WaitExample() {
    fmt.Println("=== Wait方法执行流程 ===")
    
    // Goroutine 1: 展示Wait的执行步骤
    go func() {
        cd.mu.Lock()
        fmt.Println("步骤1: 获取锁成功")
        
        for cd.value < 5 {
            fmt.Println("步骤2: 检查条件不满足 (value < 5)")
            fmt.Println("步骤3: 调用cond.Wait()")
            fmt.Println("  - 将当前goroutine加入等待队列")
            fmt.Println("  - 释放锁")
            
            cd.cond.Wait() // 这里会阻塞
            
            fmt.Println("步骤6: 被唤醒,重新获取锁")
            fmt.Println("步骤7: 再次检查条件")
        }
        
        fmt.Println("步骤8: 条件满足,执行临界区代码")
        cd.mu.Unlock()
    }()
    
    // Goroutine 2: 修改条件
    time.Sleep(100 * time.Millisecond)
    
    go func() {
        time.Sleep(1 * time.Second)
        
        cd.mu.Lock()
        fmt.Println("步骤4: 另一个goroutine获取锁")
        cd.value = 10
        fmt.Println("步骤5: 修改条件并调用cond.Signal()")
        cd.cond.Signal() // 唤醒等待的goroutine
        cd.mu.Unlock()
    }()
    
    time.Sleep(3 * time.Second)
}

func main() {
    cd := &ConditionDetails{}
    cd.cond = sync.NewCond(&cd.mu)
    cd.WaitExample()
}

https://gitee.com/rxbook/go-demo-2025/blob/master/demo/concurrent2/demo02_cond/main2.go

复制代码
go run demo02_cond/main2.go 
=== Wait方法执行流程 ===
步骤1: 获取锁成功
步骤2: 检查条件不满足 (value < 5)
步骤3: 调用cond.Wait()
  - 将当前goroutine加入等待队列
  - 释放锁
步骤4: 另一个goroutine获取锁
步骤5: 修改条件并调用cond.Signal()
步骤6: 被唤醒,重新获取锁
步骤7: 再次检查条件
步骤8: 条件满足,执行临界区代码

关键点说明:

  • 步骤3和5之间的间隙:Wait()释放锁后,其他goroutine可以获取锁并修改条件
  • 重新检查条件:被唤醒后必须重新检查条件,因为可能有多个goroutine等待同一个条件
  • 原子性:条件检查和Wait()调用必须在同一个锁的保护下

条件变量的数据结构

条件变量在Go中的实现包含了几个关键组件:
等待队列结构 head *sudog tail *sudog lock mutex sync.Cond结构体 L sync.Locker notify notifyList 关联的互斥锁 wait uint32 notify uint32 保护共享数据 等待的goroutine链表

内部实现要点:

  1. notifyList:维护等待的goroutine队列
  2. L字段:关联的锁,用于保护条件检查
  3. 原子操作:使用原子操作管理等待者计数,避免锁竞争

条件变量的经典应用场景

生产者-消费者模型(有界缓冲区)

这是条件变量最经典的应用,解决生产者和消费者之间的协调问题。当缓冲区满时,生产者需要等待;当缓冲区空时,消费者需要等待。

功能描述

  • 实现一个有固定容量的缓冲区
  • 生产者生产数据放入缓冲区
  • 消费者从缓冲区取出数据消费
  • 使用两个条件变量分别管理缓冲区"非空"和"未满"状态

注意事项

  • 使用两个条件变量分别通知生产者和消费者
  • 生产者和消费者都使用for循环检查条件
  • 在修改共享状态后立即发送信号

下面是完整的实现:

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

// BoundedBuffer 有界缓冲区
type BoundedBuffer struct {
    buffer     []interface{}
    capacity   int
    mu         sync.Mutex
    notEmpty   *sync.Cond // 缓冲区非空条件
    notFull    *sync.Cond // 缓冲区未满条件
    count      int        // 当前元素数量
    putIndex   int        // 下一个放入的位置
    takeIndex  int        // 下一个取出的位置
}

func NewBoundedBuffer(capacity int) *BoundedBuffer {
    bb := &BoundedBuffer{
        buffer:    make([]interface{}, capacity),
        capacity:  capacity,
    }
    bb.notEmpty = sync.NewCond(&bb.mu)
    bb.notFull = sync.NewCond(&bb.mu)
    return bb
}

func (bb *BoundedBuffer) Put(item interface{}) {
    bb.mu.Lock()
    defer bb.mu.Unlock()
    
    // 等待缓冲区未满
    for bb.count == bb.capacity {
        fmt.Printf("生产者: 缓冲区已满(%d/%d),等待...\n", 
                   bb.count, bb.capacity)
        bb.notFull.Wait()
    }
    
    // 放入数据
    bb.buffer[bb.putIndex] = item
    bb.putIndex = (bb.putIndex + 1) % bb.capacity
    bb.count++
    
    fmt.Printf("生产者: 放入 %v,当前数量: %d/%d\n", 
               item, bb.count, bb.capacity)
    
    // 通知消费者缓冲区非空
    bb.notEmpty.Signal()
}

func (bb *BoundedBuffer) Take() interface{} {
    bb.mu.Lock()
    defer bb.mu.Unlock()
    
    // 等待缓冲区非空
    for bb.count == 0 {
        fmt.Printf("消费者: 缓冲区为空(0/%d),等待...\n", bb.capacity)
        bb.notEmpty.Wait()
    }
    
    // 取出数据
    item := bb.buffer[bb.takeIndex]
    bb.buffer[bb.takeIndex] = nil // 帮助垃圾回收
    bb.takeIndex = (bb.takeIndex + 1) % bb.capacity
    bb.count--
    
    fmt.Printf("消费者: 取出 %v,当前数量: %d/%d\n", 
               item, bb.count, bb.capacity)
    
    // 通知生产者缓冲区未满
    bb.notFull.Signal()
    
    return item
}

func (bb *BoundedBuffer) Stats() (count, capacity int) {
    bb.mu.Lock()
    defer bb.mu.Unlock()
    return bb.count, bb.capacity
}

func producerConsumerExample() {
    fmt.Println("=== 生产者-消费者模型(有界缓冲区)===")
    
    buffer := NewBoundedBuffer(5)
    
    var wg sync.WaitGroup
    
    // 启动生产者
    producers := 3
    for i := 0; i < producers; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 4; j++ {
                item := fmt.Sprintf("产品-%d-%d", id, j)
                buffer.Put(item)
                time.Sleep(time.Duration(id+1) * 100 * time.Millisecond)
            }
        }(i)
    }
    
    // 启动消费者
    consumers := 2
    for i := 0; i < consumers; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 6; j++ {
                item := buffer.Take()
                fmt.Printf("消费者%d: 处理 %v\n", id, item)
                time.Sleep(300 * time.Millisecond)
            }
        }(i)
    }
    
    // 监控缓冲区状态
    go func() {
        ticker := time.NewTicker(500 * time.Millisecond)
        defer ticker.Stop()
        
        for range ticker.C {
            count, capacity := buffer.Stats()
            fmt.Printf("[监控] 缓冲区使用率: %d/%d (%.1f%%)\n", 
                       count, capacity, float64(count)/float64(capacity)*100)
        }
    }()
    
    wg.Wait()
    fmt.Println("所有生产消费完成")
}

func main() {
    producerConsumerExample()
}

https://gitee.com/rxbook/go-demo-2025/blob/master/demo/concurrent2/demo02_cond/main3.go

复制代码
go run demo02_cond/main3.go 
=== 生产者-消费者模型(有界缓冲区)===
生产者: 放入 产品-1-0,当前数量: 1/5
生产者: 放入 产品-0-0,当前数量: 2/5
生产者: 放入 产品-2-0,当前数量: 3/5
消费者: 取出 产品-1-0,当前数量: 2/5
消费者1: 处理 产品-1-0
消费者: 取出 产品-0-0,当前数量: 1/5
消费者0: 处理 产品-0-0
生产者: 放入 产品-0-1,当前数量: 2/5
生产者: 放入 产品-1-1,当前数量: 3/5
... ...

设计要点:

  1. 环形缓冲区:使用putIndex和takeIndex实现循环使用
  2. 两个条件变量
    • notEmpty:消费者等待缓冲区非空
    • notFull:生产者等待缓冲区未满
  3. Signal时机:在修改状态后立即发送信号,确保及时唤醒等待者

生产者-消费者模型状态图:
消费者行为 生产者行为 是 否 是 否 缓冲区空? 消费者尝试Take 等待notEmpty条件 取出数据 发送notFull信号 缓冲区满? 生产者尝试Put 等待notFull条件 放入数据 发送notEmpty信号

工作池(Worker Pool)的实现

使用场景:当需要处理大量任务,但希望控制并发度,避免系统过载时,可以使用工作池模式。

功能描述

  • 创建工作池,包含固定数量的worker
  • 将任务提交到任务队列
  • worker从队列获取任务并执行
  • 当队列满时,新任务进入等待状态

注意事项

  • 需要处理任务队列满和空的情况
  • 使用条件变量协调生产者和消费者
  • 需要优雅地关闭工作池

下面是工作池的完整实现:

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

// Task 任务定义
type Task struct {
    ID      int
    Payload string
}

// WorkerPool 工作池
type WorkerPool struct {
    workers    int
    taskQueue  chan Task
    taskCond   *sync.Cond
    taskMutex  sync.Mutex
    pending    []Task
    wg         sync.WaitGroup
    stop       chan struct{}
}

func NewWorkerPool(workers, queueSize int) *WorkerPool {
    wp := &WorkerPool{
        workers:   workers,
        taskQueue: make(chan Task, queueSize),
        stop:      make(chan struct{}),
    }
    wp.taskCond = sync.NewCond(&wp.taskMutex)
    return wp
}

func (wp *WorkerPool) worker(id int) {
    defer wp.wg.Done()
    
    fmt.Printf("Worker %d 启动\n", id)
    
    for {
        select {
        case task, ok := <-wp.taskQueue:
            if !ok {
                fmt.Printf("Worker %d 退出\n", id)
                return
            }
            
            // 处理任务
            fmt.Printf("Worker %d 开始处理任务 %d: %s\n", 
                      id, task.ID, task.Payload)
            time.Sleep(500 * time.Millisecond) // 模拟处理时间
            fmt.Printf("Worker %d 完成处理任务 %d\n", id, task.ID)
            
            // 检查是否有待处理的任务
            wp.taskMutex.Lock()
            if len(wp.pending) > 0 {
                // 将待处理任务加入队列
                select {
                case wp.taskQueue <- wp.pending[0]:
                    wp.pending = wp.pending[1:]
                    wp.taskCond.Signal() // 通知有新空间
                default:
                    // 队列已满,保持pending状态
                }
            }
            wp.taskMutex.Unlock()
            
        case <-wp.stop:
            fmt.Printf("Worker %d 收到停止信号\n", id)
            return
        }
    }
}

func (wp *WorkerPool) Submit(task Task) bool {
    wp.taskMutex.Lock()
    defer wp.taskMutex.Unlock()
    
    // 尝试直接放入队列
    select {
    case wp.taskQueue <- task:
        return true
    default:
        // 队列已满,放入pending列表
        wp.pending = append(wp.pending, task)
        fmt.Printf("任务 %d 进入等待队列,当前等待数: %d\n", 
                  task.ID, len(wp.pending))
        
        // 等待队列有空间
        for len(wp.pending) > 0 {
            select {
            case wp.taskQueue <- wp.pending[0]:
                wp.pending = wp.pending[1:]
                fmt.Printf("等待任务 %d 进入队列成功\n", task.ID)
                return true
            default:
                // 队列仍然满,等待
                fmt.Printf("等待队列空间...\n")
                wp.taskCond.Wait()
            }
        }
    }
    
    return false
}

func (wp *WorkerPool) Start() {
    wp.wg.Add(wp.workers)
    for i := 0; i < wp.workers; i++ {
        go wp.worker(i)
    }
}

func (wp *WorkerPool) Stop() {
    close(wp.stop)
    close(wp.taskQueue)
    wp.wg.Wait()
    fmt.Println("工作池已停止")
}

func (wp *WorkerPool) Stats() (queueLen, pendingLen int) {
    wp.taskMutex.Lock()
    defer wp.taskMutex.Unlock()
    return len(wp.taskQueue), len(wp.pending)
}

func workerPoolExample() {
    fmt.Println("=== 工作池示例 ===")
    
    pool := NewWorkerPool(3, 5) // 3个worker,队列容量5
    pool.Start()
    defer pool.Stop()
    
    // 提交任务
    var submitWg sync.WaitGroup
    
    for i := 0; i < 20; i++ {
        submitWg.Add(1)
        go func(id int) {
            defer submitWg.Done()
            
            task := Task{
                ID:      id,
                Payload: fmt.Sprintf("任务数据-%d", id),
            }
            
            if pool.Submit(task) {
                fmt.Printf("任务 %d 提交成功\n", id)
            } else {
                fmt.Printf("任务 %d 提交失败\n", id)
            }
        }(i)
        
        time.Sleep(50 * time.Millisecond) // 控制提交速度
    }
    
    // 监控状态
    go func() {
        ticker := time.NewTicker(1 * time.Second)
        defer ticker.Stop()
        
        for range ticker.C {
            queueLen, pendingLen := pool.Stats()
            fmt.Printf("[监控] 队列长度: %d, 等待任务: %d\n", 
                      queueLen, pendingLen)
        }
    }()
    
    submitWg.Wait()
    fmt.Println("所有任务提交完成")
    
    // 等待任务处理完成
    time.Sleep(3 * time.Second)
}

func main() {
    workerPoolExample()
}

https://gitee.com/rxbook/go-demo-2025/blob/master/demo/concurrent2/demo02_cond/main4.go

复制代码
go run demo02_cond/main4.go
=== 工作池示例 ===
任务 0 提交成功
Worker 2 启动
Worker 2 开始处理任务 0: 任务数据-0
Worker 1 启动
Worker 0 启动
任务 1 提交成功
Worker 1 开始处理任务 1: 任务数据-1
任务 2 提交成功
Worker 0 开始处理任务 2: 任务数据-2
任务 3 提交成功
... ... 

工作池状态流转图:
worker启动 获取到任务 任务完成 收到停止信号 空闲状态 等待任务 处理任务 条件变量等待:

  1. 任务队列非空
  2. 收到停止信号 任务执行中:
  3. 执行用户任务
  4. 检查等待队列
  5. 唤醒等待的提交者

条件变量的高级用法

超时等待的实现

使用场景:有时候我们不想无限期地等待条件成立,而是希望在指定时间内等待,超时后执行其他操作。

注意事项

  • 标准sync.Cond不支持超时
  • 需要结合select和time.After实现
  • 需要注意goroutine泄漏问题

下面是超时等待的实现示例:

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

// CondWithTimeout 支持超时的条件变量
type CondWithTimeout struct {
    cond *sync.Cond
}

func (cwt *CondWithTimeout) WaitWithTimeout(timeout time.Duration) bool {
    // 创建一个通知通道
    ch := make(chan struct{})
    
    // 启动一个goroutine来等待条件变量
    go func() {
        cwt.cond.L.Lock()
        defer cwt.cond.L.Unlock()
        
        cwt.cond.Wait()
        ch <- struct{}{}
    }()
    
    // 等待条件变量或超时
    select {
    case <-ch:
        return true // 条件满足
    case <-time.After(timeout):
        // 超时,需要取消等待
        cwt.cond.L.Lock()
        defer cwt.cond.L.Unlock()
        
        // 这里无法真正取消Wait,但可以设置一个标志让goroutine在唤醒后立即退出
        // 在实际应用中,可能需要更复杂的机制
        return false
    }
}

func timeoutExample() {
    fmt.Println("=== 超时等待示例 ===")
    
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    cwt := &CondWithTimeout{cond: cond}
    
    var condition bool
    
    // 等待者
    go func() {
        mu.Lock()
        defer mu.Unlock()
        
        fmt.Println("等待者: 开始等待(最多2秒)")
        
        if cwt.WaitWithTimeout(2 * time.Second) {
            if condition {
                fmt.Println("等待者: 条件满足")
            } else {
                fmt.Println("等待者: 被唤醒但条件不满足")
            }
        } else {
            fmt.Println("等待者: 等待超时")
        }
    }()
    
    // 条件设置者(故意延迟3秒,超过等待时间)
    go func() {
        time.Sleep(3 * time.Second)
        
        mu.Lock()
        condition = true
        fmt.Println("设置者: 设置条件为true")
        mu.Unlock()
        
        cond.Signal()
        fmt.Println("设置者: 发送信号")
    }()
    
    time.Sleep(4 * time.Second)
}

func main() {
    timeoutExample()
}

https://gitee.com/rxbook/go-demo-2025/blob/master/demo/concurrent2/demo02_cond/main5_1.go

超时机制实现原理:
问题分析 cond.Wait完成 time.After超时 原问题: goroutine泄漏 cond.Wait无法取消 后台goroutine永远阻塞 开始超时等待 启动等待goroutine 创建通知通道ch goroutine调用cond.Wait select等待 ch接收到信号 返回true 返回false goroutine泄漏风险

注意: 上述实现有goroutine泄漏的风险,因为超时后无法取消已经调用的cond.Wait()。这段代码仅供演示,在实际应用中,需要使用更复杂的技术。

条件变量的广播机制

使用场景:当有多个goroutine等待同一个条件,且条件满足时需要唤醒所有等待者时,使用Broadcast。

功能描述

  • Signal:只唤醒等待队列中的一个goroutine
  • Broadcast:唤醒等待队列中的所有goroutine

注意事项

  • 使用Broadcast时要确保确实需要唤醒所有等待者
  • 被唤醒的goroutine需要重新检查条件(使用for循环)
  • Broadcast的性能开销通常比Signal大

下面是Broadcast和Signal的对比示例:

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

func broadcastExample() {
    fmt.Println("=== Broadcast广播机制示例 ===")
    
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    
    var counter int
    target := 5
    
    // 启动多个等待者
    for i := 0; i < 5; i++ {
        go func(id int) {
            mu.Lock()
            defer mu.Unlock()
            
            fmt.Printf("等待者 %d: 开始等待\n", id)
            
            // 等待条件:counter >= target
            for counter < target {
                cond.Wait()
                fmt.Printf("等待者 %d: 被唤醒,检查条件 counter=%d\n", 
                          id, counter)
            }
            
            fmt.Printf("等待者 %d: 条件满足,继续执行\n", id)
        }(i)
    }
    
    // 给等待者一些时间进入等待状态
    time.Sleep(100 * time.Millisecond)
    
    // 逐步增加counter
    go func() {
        for i := 0; i <= target; i++ {
            time.Sleep(500 * time.Millisecond)
            
            mu.Lock()
            counter = i
            fmt.Printf("设置者: 设置 counter=%d\n", counter)
            mu.Unlock()
            
            if i == target {
                // 当条件满足时,广播通知所有等待者
                fmt.Println("设置者: 条件满足,广播通知所有等待者")
                cond.Broadcast()
            } else {
                // 条件未满足,只通知一个等待者(通常用于测试)
                cond.Signal()
            }
        }
    }()
    
    time.Sleep(5 * time.Second)
}

// Broadcast与Signal的区别示例
func broadcastVsSignal() {
    fmt.Println("\n=== Broadcast vs Signal 区别 ===")
    
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    
    var resourceAvailable bool
    
    fmt.Println("场景1: 使用Signal(只唤醒一个)")
    for i := 0; i < 3; i++ {
        go func(id int) {
            mu.Lock()
            defer mu.Unlock()
            
            for !resourceAvailable {
                fmt.Printf("Goroutine %d: 等待资源\n", id)
                cond.Wait()
            }
            
            fmt.Printf("Goroutine %d: 获取资源\n", id)
        }(i)
    }
    
    time.Sleep(100 * time.Millisecond)
    
    // 使用Signal只唤醒一个
    mu.Lock()
    resourceAvailable = true
    fmt.Println("设置资源可用(Signal)")
    mu.Unlock()
    cond.Signal()
    
    time.Sleep(1 * time.Second)
    
    fmt.Println("\n场景2: 使用Broadcast(唤醒所有)")
    resourceAvailable = false
    
    for i := 3; i < 6; i++ {
        go func(id int) {
            mu.Lock()
            defer mu.Unlock()
            
            for !resourceAvailable {
                fmt.Printf("Goroutine %d: 等待资源\n", id)
                cond.Wait()
            }
            
            fmt.Printf("Goroutine %d: 获取资源\n", id)
        }(i)
    }
    
    time.Sleep(100 * time.Millisecond)
    
    // 使用Broadcast唤醒所有
    mu.Lock()
    resourceAvailable = true
    fmt.Println("设置资源可用(Broadcast)")
    mu.Unlock()
    cond.Broadcast()
    
    time.Sleep(1 * time.Second)
}

func main() {
    broadcastExample()
    broadcastVsSignal()
}

https://gitee.com/rxbook/go-demo-2025/blob/master/demo/concurrent2/demo02_cond/main6.go

复制代码
go run demo02_cond/main6.go
=== Broadcast广播机制示例 ===
等待者 1: 开始等待
等待者 4: 开始等待
等待者 2: 开始等待
等待者 3: 开始等待
等待者 0: 开始等待
设置者: 设置 counter=0
等待者 1: 被唤醒,检查条件 counter=0
设置者: 设置 counter=1
等待者 4: 被唤醒,检查条件 counter=1
设置者: 设置 counter=2
... ...

Signal vs Broadcast 对比图:
Broadcast机制 Signal机制 有等待者 无等待者 有等待者 无等待者 等待队列状态 调用cond.Broadcast 唤醒所有等待者 信号丢失 所有等待者竞争锁 等待队列状态 调用cond.Signal 唤醒队列中的第一个 信号丢失 被唤醒者竞争锁

使用选择建议:

  1. 使用Signal的情况
    • 只有一个等待者
    • 条件满足后只需一个goroutine继续执行
    • 性能要求高(Broadcast开销更大)
  2. 使用Broadcast的情况
    • 多个等待者都需要知道条件变化
    • 条件变化后所有等待者都可能满足条件
    • 不确定哪个等待者应该被唤醒

面试常见问题与闭坑指南

面试常见问题

Q1: 简单描述下sync.Cond的用法
  1. sync.Cond的本质:基于锁的等待/通知机制,用于goroutine间的条件同步

  2. 三个核心方法Wait():释放锁并等待;Signal():唤醒一个等待者;Broadcast():唤醒所有等待者

  3. 正确使用模式

go 复制代码
mu.Lock()
for !condition {  // 必须用for
    cond.Wait()
}
// 执行操作
mu.Unlock()
Q2: sync.Cond 和 channel 有什么区别?分别在什么场景下使用?

参考答案:

特性 sync.Cond channel
主要用途 基于条件的等待/通知机制 数据传输和同步
性能 较高,直接操作goroutine队列 较低,有缓冲和调度开销
灵活性 高,可基于任意条件等待 中,主要用于数据流
复杂度 较高,需要正确处理锁和条件 较低,API简单直观
一对多通信 容易(Broadcast) 需要额外设计
超时支持 需要自己实现 内置(select + time.After)

使用场景:

  • 使用sync.Cond
    1. 复杂的条件等待(如:等待缓冲区非空)
    2. 需要唤醒一个或多个特定的goroutine
    3. 性能敏感的场景
    4. 一对多通知(Broadcast)
  • 使用channel
    1. 简单的数据传递
    2. 一对一的goroutine同步
    3. 需要select多路复用
    4. 简单的超时控制

代码示例对比:

go 复制代码
// 使用sync.Cond实现等待
var mu sync.Mutex
cond := sync.NewCond(&mu)
mu.Lock()
for !condition {
    cond.Wait()
}
// 处理...
mu.Unlock()

// 使用channel实现等待
ch := make(chan struct{})
go func() {
    // 等待条件...
    ch <- struct{}{}
}()
<-ch  // 阻塞等待
Q: 什么是虚假唤醒?为什么需要用for循环而不是if检查条件?

参考答案:

虚假唤醒(Spurious Wakeup):指等待的goroutine在没有收到Signal或Broadcast的情况下被唤醒的现象。这是操作系统层面的特性,不是Go特有的。

为什么用for循环:

  1. 防止虚假唤醒:操作系统可能因为各种原因(如信号中断)唤醒线程
  2. 条件可能再次变化:被唤醒后条件可能已经被其他goroutine改变
  3. 多个等待者竞争:Broadcast唤醒所有等待者,但只有一个能获得资源
go 复制代码
// 错误:使用if
mu.Lock()
if !condition {  // 这里可能虚假唤醒
    cond.Wait()
}
// 执行操作(可能条件不满足)
mu.Unlock()

// 正确:使用for
mu.Lock()
for !condition {  // 每次唤醒都重新检查
    cond.Wait()
}
// 这里条件一定满足
mu.Unlock()

虚假唤醒示意图:
不满足 满足 goroutine等待 Wait释放锁 进入等待队列 虚假唤醒 重新获取锁 条件检查 执行操作

Q: sync.Cond的Wait方法为什么要先释放锁,唤醒后再获取锁?

参考答案:

Wait方法的设计原理:

go 复制代码
// Wait方法的伪代码实现
func (c *Cond) Wait() {
    // 1. 将当前goroutine加入等待队列
    c.addToWaitQueue()
    
    // 2. 释放锁(允许其他goroutine修改条件)
    c.L.Unlock()
    
    // 3. 阻塞当前goroutine
    parkCurrentGoroutine()
    
    // 4. 被唤醒后重新获取锁
    c.L.Lock()
}

为什么这样设计:

  1. 避免死锁:如果不释放锁,其他goroutine无法获取锁来修改条件
  2. 提高并发性:释放锁允许其他goroutine并行工作
  3. 保证原子性:检查和进入等待是原子的,防止竞态条件

执行流程:

text

复制代码
时间线:     持有锁的goroutine A | 其他goroutine B
           ──────────────────────────────────
第1步:     检查条件(不满足)
第2步:     调用Wait()释放锁
第3步:     阻塞等待
第4步:                         		获取锁
第5步:                         		修改条件
第6步:                         		调用Signal()
第7步:                         		释放锁
第8步:     被唤醒
第9步:     重新获取锁
第10步:    再次检查(满足)
第11步:    执行操作
第12步:    释放锁
Q5: 如何优雅地关闭使用sync.Cond的程序?

参考答案:

关闭模式设计:

go 复制代码
type SafeService struct {
    mu       sync.Mutex
    cond     *sync.Cond
    shutdown bool
    workers  []*worker
}

func (s *SafeService) Shutdown() {
    s.mu.Lock()
    s.shutdown = true
    s.mu.Unlock()
    
    // 唤醒所有等待的goroutine
    s.cond.Broadcast()
    
    // 等待所有goroutine退出
    for _, w := range s.workers {
        w.wait()
    }
}

func (s *SafeService) worker() {
    for {
        s.mu.Lock()
        
        // 检查关闭标志
        for !s.hasWork() && !s.shutdown {
            s.cond.Wait()
        }
        
        if s.shutdown {
            s.mu.Unlock()
            return // 优雅退出
        }
        
        // 处理工作...
        s.mu.Unlock()
    }
}

关键点:

  1. 设置关闭标志:在锁保护下设置shutdown标志
  2. 广播唤醒:使用Broadcast唤醒所有等待者
  3. 重新检查:被唤醒的goroutine检查shutdown标志
  4. 等待退出:主goroutine等待所有worker退出
Q: sync.Cond的性能考虑有哪些?

参考答案:

性能优化要点:减少锁竞争:

go 复制代码
// 不好:长时间持有锁
mu.Lock()
data := loadHeavyData()  // 耗时操作
cond.Signal()
mu.Unlock()

// 好:减少锁持有时间
data := loadHeavyData()  // 在锁外执行
mu.Lock()
cond.Signal()
mu.Unlock()

选择合适的唤醒策略

  • 单个消费者:使用Signal
  • 多个消费者:考虑使用Broadcast,但注意竞争

避免不必要的唤醒

go 复制代码
// 只在条件真正变化时发送信号
if oldValue != newValue {
    cond.Signal()
}

使用sync.Pool减少对象分配

go 复制代码
var condPool = sync.Pool{
    New: func() interface{} {
        return sync.NewCond(&sync.Mutex{})
    },
}
Q:如何优化高并发下sync.Cond的性能?

参考答案:

  1. 减少锁粒度:使用读写锁或细粒度锁
  2. 批量处理:积累多个信号后批量唤醒
  3. 避免饥饿:确保等待时间长的goroutine有机会执行
  4. 使用无锁数据结构:在可能的情况下使用原子操作
  5. 监控和调优:使用pprof分析锁竞争
go 复制代码
// 示例:批量信号优化
type BatchCond struct {
    mu      sync.Mutex
    cond    *sync.Cond
    pending int
}

func (bc *BatchCond) SignalBatch(threshold int) {
    bc.mu.Lock()
    if bc.pending >= threshold {
        bc.cond.Broadcast()
        bc.pending = 0
    }
    bc.mu.Unlock()
}

闭坑指南

坑1:忘记在for循环中检查条件

问题:

go 复制代码
mu.Lock()
if !ready {  // 错误:应该用for
    cond.Wait()
}
// 这里ready可能还是false(虚假唤醒)
mu.Unlock()

解决:

go 复制代码
mu.Lock()
for !ready {  // 正确:使用for
    cond.Wait()
}
// 这里ready一定是true
mu.Unlock()
坑2:在错误的时间发送信号

问题:

go 复制代码
mu.Lock()
ready = true
mu.Unlock()
// 这里可能发生上下文切换,导致信号在Wait之前发送
cond.Signal()

解决:

go 复制代码
mu.Lock()
ready = true
cond.Signal()  // 在锁内发送信号
mu.Unlock()
坑3:多个条件共用一个条件变量

问题:

go 复制代码
// 多个条件共用一个cond,难以管理
var cond = sync.NewCond(&mu)
var ready1, ready2 bool

// 等待ready1,但可能被ready2的信号唤醒

解决:

go 复制代码
// 为每个条件创建单独的cond
var cond1 = sync.NewCond(&mu)
var cond2 = sync.NewCond(&mu)

// 或者使用一个cond但明确检查条件
for !ready1 && !ready2 {
    cond.Wait()
}
坑4:没有正确处理Broadcast的竞争

问题: Broadcast唤醒所有等待者,但只有一个能获得资源,其他会重新进入等待。

解决:

go 复制代码
// 使用公平调度或限流
type ResourcePool struct {
    mu      sync.Mutex
    cond    *sync.Cond
    resources []Resource
    waiters  int
}

func (p *ResourcePool) Get() Resource {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    for len(p.resources) == 0 {
        p.waiters++
        p.cond.Wait()
        p.waiters--
        
        // 被唤醒后重新检查
        if len(p.resources) > 0 {
            break
        }
    }
    
    // 获取资源...
    return resource
}
坑5:goroutine泄漏

问题: 忘记了让等待的goroutine退出。

解决:

go 复制代码
type Service struct {
    mu       sync.Mutex
    cond     *sync.Cond
    shutdown bool
}

func (s *Service) Stop() {
    s.mu.Lock()
    s.shutdown = true
    s.cond.Broadcast()  // 唤醒所有等待者
    s.mu.Unlock()
}

func (s *Service) WaitForCondition() {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    for !s.condition && !s.shutdown {
        s.cond.Wait()
    }
    
    if s.shutdown {
        return // 优雅退出
    }
    
    // 处理...
}

面试题目练习

题目1:实现一个并发安全的计数器,支持等待达到特定值
go 复制代码
// 要求:多个goroutine等待计数器达到特定值
type WaitableCounter struct {
    mu    sync.Mutex
    cond  *sync.Cond
    value int
    // 添加你的代码...
}

func (wc *WaitableCounter) Increment() {
    wc.mu.Lock()
    wc.value++
    wc.cond.Broadcast() // 唤醒所有等待者
    wc.mu.Unlock()
}

func (wc *WaitableCounter) WaitUntil(target int) {
    wc.mu.Lock()
    defer wc.mu.Unlock()
    
    for wc.value < target {
        wc.cond.Wait()
    }
}
题目2:使用sync.Cond实现一个简单的连接池
go 复制代码
type ConnectionPool struct {
    mu      sync.Mutex
    cond    *sync.Cond
    conns   chan net.Conn
    maxSize int
    // 添加你的代码...
}

func (p *ConnectionPool) Get() (net.Conn, error) {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    // 等待有可用连接或可以创建新连接
    for len(p.conns) == 0 && p.active >= p.maxSize {
        p.cond.Wait()
    }
    
    // 获取或创建连接...
    return conn, nil
}
题目3:实现一个多生产者-多消费者的任务队列
go 复制代码
type TaskQueue struct {
    mu       sync.Mutex
    notEmpty *sync.Cond  // 队列非空条件
    notFull  *sync.Cond  // 队列未满条件
    tasks    []Task
    capacity int
}

func (q *TaskQueue) Put(task Task) {
    q.mu.Lock()
    defer q.mu.Unlock()
    
    for len(q.tasks) == q.capacity {
        q.notFull.Wait()
    }
    
    q.tasks = append(q.tasks, task)
    q.notEmpty.Signal()
}

func (q *TaskQueue) Get() Task {
    q.mu.Lock()
    defer q.mu.Unlock()
    
    for len(q.tasks) == 0 {
        q.notEmpty.Wait()
    }
    
    task := q.tasks[0]
    q.tasks = q.tasks[1:]
    q.notFull.Signal()
    
    return task
}

本文的源代码:https://gitee.com/rxbook/go-demo-2025/tree/master/demo/concurrent2/demo02_cond

相关推荐
自由生长20242 小时前
请求洪峰来了,怎么使用消息队列削峰? 我们来深入的聊一下
后端·架构
Victor3563 小时前
Netty(28)Netty的内存管理和垃圾回收机制是如何工作的?
后端
后端小张3 小时前
【JAVA 进阶】SpringMVC全面解析:从入门到实战的核心知识点梳理
java·开发语言·spring boot·spring·spring cloud·java-ee·springmvc
2301_789015623 小时前
C++:二叉搜索树
c语言·开发语言·数据结构·c++·算法·排序算法
帅那个帅4 小时前
PHP里面的抽象类和接口类
开发语言·php
咖啡の猫10 小时前
Python字典推导式
开发语言·python
leiming610 小时前
C++ vector容器
开发语言·c++·算法
掘金码甲哥10 小时前
🚀糟糕,我实现的k8s informer好像是依托答辩
后端