文章目录
-
- 条件变量的基本概念
- 条件变量的内部工作原理
- 条件变量的经典应用场景
-
- 生产者-消费者模型(有界缓冲区)
- [工作池(Worker Pool)的实现](#工作池(Worker Pool)的实现)
- 条件变量的高级用法
- 面试常见问题与闭坑指南
-
- 面试常见问题
-
- [Q1: 简单描述下sync.Cond的用法](#Q1: 简单描述下sync.Cond的用法)
- [Q2: sync.Cond 和 channel 有什么区别?分别在什么场景下使用?](#Q2: sync.Cond 和 channel 有什么区别?分别在什么场景下使用?)
- [Q: 什么是虚假唤醒?为什么需要用for循环而不是if检查条件?](#Q: 什么是虚假唤醒?为什么需要用for循环而不是if检查条件?)
- [Q: sync.Cond的Wait方法为什么要先释放锁,唤醒后再获取锁?](#Q: sync.Cond的Wait方法为什么要先释放锁,唤醒后再获取锁?)
- [Q5: 如何优雅地关闭使用sync.Cond的程序?](#Q5: 如何优雅地关闭使用sync.Cond的程序?)
- [Q: sync.Cond的性能考虑有哪些?](#Q: sync.Cond的性能考虑有哪些?)
- Q:如何优化高并发下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方法的执行步骤:
- 获取关联的互斥锁
- 检查条件是否满足
- 如果条件不满足,将当前goroutine加入等待队列
- 释放互斥锁并阻塞当前goroutine
- 被唤醒后重新获取互斥锁
- 再次检查条件(防止虚假唤醒)
下面是详细的代码演示:
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链表
内部实现要点:
- notifyList:维护等待的goroutine队列
- L字段:关联的锁,用于保护条件检查
- 原子操作:使用原子操作管理等待者计数,避免锁竞争
条件变量的经典应用场景
生产者-消费者模型(有界缓冲区)
这是条件变量最经典的应用,解决生产者和消费者之间的协调问题。当缓冲区满时,生产者需要等待;当缓冲区空时,消费者需要等待。
功能描述:
- 实现一个有固定容量的缓冲区
- 生产者生产数据放入缓冲区
- 消费者从缓冲区取出数据消费
- 使用两个条件变量分别管理缓冲区"非空"和"未满"状态
注意事项:
- 使用两个条件变量分别通知生产者和消费者
- 生产者和消费者都使用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 ... ...
设计要点:
- 环形缓冲区:使用putIndex和takeIndex实现循环使用
- 两个条件变量 :
notEmpty:消费者等待缓冲区非空notFull:生产者等待缓冲区未满
- 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启动 获取到任务 任务完成 收到停止信号 空闲状态 等待任务 处理任务 条件变量等待:
- 任务队列非空
- 收到停止信号 任务执行中:
- 执行用户任务
- 检查等待队列
- 唤醒等待的提交者
条件变量的高级用法
超时等待的实现
使用场景:有时候我们不想无限期地等待条件成立,而是希望在指定时间内等待,超时后执行其他操作。
注意事项:
- 标准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 唤醒队列中的第一个 信号丢失 被唤醒者竞争锁
使用选择建议:
- 使用Signal的情况 :
- 只有一个等待者
- 条件满足后只需一个goroutine继续执行
- 性能要求高(Broadcast开销更大)
- 使用Broadcast的情况 :
- 多个等待者都需要知道条件变化
- 条件变化后所有等待者都可能满足条件
- 不确定哪个等待者应该被唤醒
面试常见问题与闭坑指南
面试常见问题
Q1: 简单描述下sync.Cond的用法
-
sync.Cond的本质:基于锁的等待/通知机制,用于goroutine间的条件同步
-
三个核心方法 :
Wait():释放锁并等待;Signal():唤醒一个等待者;Broadcast():唤醒所有等待者 -
正确使用模式:
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 :
- 复杂的条件等待(如:等待缓冲区非空)
- 需要唤醒一个或多个特定的goroutine
- 性能敏感的场景
- 一对多通知(Broadcast)
- 使用channel :
- 简单的数据传递
- 一对一的goroutine同步
- 需要select多路复用
- 简单的超时控制
代码示例对比:
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循环:
- 防止虚假唤醒:操作系统可能因为各种原因(如信号中断)唤醒线程
- 条件可能再次变化:被唤醒后条件可能已经被其他goroutine改变
- 多个等待者竞争: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()
}
为什么这样设计:
- 避免死锁:如果不释放锁,其他goroutine无法获取锁来修改条件
- 提高并发性:释放锁允许其他goroutine并行工作
- 保证原子性:检查和进入等待是原子的,防止竞态条件
执行流程:
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()
}
}
关键点:
- 设置关闭标志:在锁保护下设置shutdown标志
- 广播唤醒:使用Broadcast唤醒所有等待者
- 重新检查:被唤醒的goroutine检查shutdown标志
- 等待退出:主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的性能?
参考答案:
- 减少锁粒度:使用读写锁或细粒度锁
- 批量处理:积累多个信号后批量唤醒
- 避免饥饿:确保等待时间长的goroutine有机会执行
- 使用无锁数据结构:在可能的情况下使用原子操作
- 监控和调优:使用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