Go 并发进阶:sync.Cond 条件变量与互斥锁的协作精髓
Go 语言中 sync.Cond 条件变量用于解决协程等待共享资源状态变更的问题,避免轮询消耗 CPU。它必须与互斥锁绑定,Wait() 需在持有锁时调用且配合 for 循环检查状态,Signal/Broadcast 需在释放锁后调用。相比 Channel,sync.Cond 更适合多对多复杂场景或需要广播通知的情况。正确使用 sync.Cond 能提升并发协作效率与代码健壮性。

Go 语言中 sync.Cond 条件变量用于解决协程等待共享资源状态变更的问题,避免轮询消耗 CPU。它必须与互斥锁绑定,Wait() 需在持有锁时调用且配合 for 循环检查状态,Signal/Broadcast 需在释放锁后调用。相比 Channel,sync.Cond 更适合多对多复杂场景或需要广播通知的情况。正确使用 sync.Cond 能提升并发协作效率与代码健壮性。

在 Go 语言并发编程中,互斥锁(sync.Mutex/sync.RWMutex)是保护共享资源的基础,但仅靠互斥锁往往无法高效解决'等待共享资源状态变更'的问题——如果线程/协程需要循环检查资源状态,会造成不必要的 CPU 消耗,甚至降低程序的并发性能。而条件变量(sync.Cond) 正是为解决这一痛点而生,它基于互斥锁实现,能在共享资源状态变化时主动通知等待的协程,让并发协作更高效、更优雅。
本文将从'原理 + 实战'双维度,拆解 sync.Cond 与互斥锁的配合逻辑,带你彻底搞懂条件变量的使用姿势。
很多初学者会把条件变量和互斥锁混为一谈,但二者的核心定位完全不同:
用一个生动的场景类比: 假设你和我执行秘密任务,共享一个'信箱'(共享资源):我负责放情报,你负责取情报,信箱同一时刻只能被一个人打开(互斥锁的作用)。如果我去放情报时发现信箱非空,没必要反复去检查(轮询),只需等你取走后收到'蓝帽子小孩'的通知;如果你去取情报时发现信箱为空,也只需等我放入后收到'红帽子小孩'的通知。这里的'红蓝帽子小孩',就是条件变量的核心——状态变更的通知器。
sync.Cond 无法单独使用,必须与互斥锁(sync.Mutex/sync.RWMutex)绑定,其核心方法(Wait/Signal/Broadcast)的使用也完全依赖互斥锁的状态:
sync.Cond 没有'开箱即用'的零值,必须通过 sync.NewCond() 函数初始化,该函数要求传入一个 sync.Locker 接口实现(互斥锁的指针):
import (
"log"
"sync"
"time"
)
// 示例 1:基于普通互斥锁初始化
var mu sync.Mutex
cond := sync.NewCond(&mu)
// 示例 2:基于读写锁的读锁/写锁初始化
var rwMu sync.RWMutex
// 绑定写锁(Lock/Unlock)
sendCond := sync.NewCond(&rwMu)
// 绑定读锁(RLock/RUnlock):通过 RLocker() 做方法代理
recvCond := sync.NewCond(rwMu.RLocker())
sync.RWMutex 的 RLocker() 方法会返回一个代理对象,其 Lock()/Unlock() 方法内部会调用读锁的 RLock()/RUnlock(),以此适配 sync.Locker 接口。
sync.Cond 提供三个核心方法,其使用必须遵循与互斥锁的配合规则:
| 方法 | 作用 | 互斥锁状态要求 |
|---|---|---|
Wait() | 等待通知 | 调用前必须持有绑定的互斥锁(已 Lock/RLock) |
Signal() | 单发通知(唤醒一个等待协程) | 调用前必须释放绑定的互斥锁(已 Unlock/RUnlock) |
Broadcast() | 广播通知(唤醒所有等待协程) | 调用前必须释放绑定的互斥锁(已 Unlock/RUnlock) |
调用 Wait() 时,协程会做三件事:
这也是为什么 Wait() 必须在'持有锁'的状态下调用——如果不持有锁,'释放锁'的操作就会 panic。
结合前文的'信箱场景',我们用代码实现完整的协程协作逻辑,拆解每一步的核心要点。
package main
import (
"log"
"sync"
"time"
)
// 信箱:0=空,1=有情报
var mailbox uint8
// 读写锁:保护 mailbox 的访问
var lock sync.RWMutex
// 条件变量:通知'可以放情报'(绑定写锁)
var sendCond = sync.NewCond(&lock)
// 条件变量:通知'可以取情报'(绑定读锁)
var recvCond = sync.NewCond(lock.RLocker())
// 放情报的协程
func sender(max int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= max; i++ {
time.Sleep(time.Millisecond * 500) // 模拟业务耗时
// 1. 持有写锁(保护 mailbox)
lock.Lock()
// 2. 检查状态:信箱非空则等待(必须用 for,而非 if!)
for mailbox == 1 {
sendCond.Wait() // 释放锁并等待通知,被唤醒后重新获取锁
}
// 3. 状态满足,操作共享资源
mailbox = 1
log.Printf("发送者 [%d]:情报已放入信箱", i)
// 4. 释放写锁
lock.Unlock()
// 5. 通知取情报的协程:状态已变更
recvCond.Signal()
}
}
// 取情报的协程
func receiver(max int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= max; i++ {
time.Sleep(time.Millisecond * 500) // 模拟业务耗时
// 1. 持有读锁(保护 mailbox)
lock.RLock()
// 2. 检查状态:信箱为空则等待(同样用 for)
for mailbox == 0 {
recvCond.Wait() // 释放读锁并等待通知
}
// 3. 状态满足,操作共享资源
mailbox = 0
log.Printf("接收者 [%d]:情报已从信箱取走", i)
// 4. 释放读锁
lock.RUnlock()
// 5. 通知放情报的协程:状态已变更
sendCond.Signal()
}
}
func main() {
var wg sync.WaitGroup
max := 5 // 收发次数
wg.Add(2)
// 启动协程
go sender(max, &wg)
go receiver(max, &wg)
// 等待协程结束
wg.Wait()
log.Println("任务完成:所有情报已收发完毕")
}
这是 sync.Cond 使用中最容易踩坑的点!
Wait() 被唤醒后不代表状态一定满足(可能存在'虚假唤醒',或多个协程被 Broadcast 唤醒后抢占资源)。比如:多个取情报的协程被唤醒,其中一个协程先获取锁并取走情报,其他协程再获取锁时,信箱已空——如果用 if,会直接执行后续操作,导致逻辑错误;而 for 循环会重新检查状态,不满足则继续等待。
示例中 sendCond 绑定写锁(对应'放情报'的写操作),recvCond 绑定读锁(对应'取情报'的读操作),既保证了共享资源的安全,又利用读写锁的特性提升了读操作的并发度(如果有多个'读情报'的协程,读锁可并发持有)。
虽然 Signal()/Broadcast() 在锁内调用也不会 panic,但解锁后调用更高效:如果在锁内调用,被唤醒的协程会立刻尝试获取锁,但此时锁还未释放,协程会再次阻塞;解锁后调用,被唤醒的协程可以直接获取锁,减少上下文切换。
*sync.Cond(指针类型):可以安全传递。因为 sync.Cond 的方法都是指针方法,且指针传递不会拷贝底层的锁和通知队列,协程间共享同一个条件变量实例;sync.Cond(值类型):禁止传递/拷贝。sync.Cond 包含 noCopy 字段(编译期检查拷贝),拷贝后会导致多个实例绑定同一个锁,引发通知逻辑混乱,甚至 panic。示例中'取情报'协程持有读锁时,将 mailbox 置为 0(写操作),看似违背'读锁只读'的原则,实则是场景简化的设计:
实际业务中,若多个协程同时'取情报',需改用写锁;示例中仅一个取情报协程,读锁仅用于配合条件变量演示,核心是理解'条件变量与读锁的绑定方式'。
sync.Cond 的核心价值是'高效等待状态变更',其与互斥锁的配合可总结为三句话:
在并发编程中,当你需要'等待共享资源状态满足后再操作'时,sync.Cond 能避免轮询带来的性能损耗,让协程协作更优雅。掌握它与互斥锁的配合逻辑,能让你的 Go 并发代码更健壮、更高效。
拓展思考:如果需要唤醒所有等待的协程(比如多个协程等待同一个信箱),该如何修改代码?(提示:用 Broadcast() 替代 Signal())

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online