当前位置: 首页 > news >正文

Go 的 sync.Cond 实战:用条件变量做等待/通知,比忙轮询省 CPU

Go 的 sync.Cond 实战:用条件变量做等待/通知,比忙轮询省 CPU

你写了个有界队列,消费者要「队列非空才取」。第一版你多半会写个for循环反复检查——CPU 直接跑满一个核。加个time.Sleep呢?延迟又上来了。这类「等某个条件成立再继续」的场景,标准库给了个专门的工具:sync.Cond。它冷门,但一旦遇上就是它最合适。这篇手把手教你用对它,以及那几个几乎人人踩的坑。

先看忙轮询有多糟

需求:一个容量固定的缓冲区,生产者往里放,消费者从里取;满了生产者等,空了消费者等。

typeBadQueuestruct{mu sync.Mutex items[]intcapint}func(q*BadQueue)Pop()int{for{q.mu.Lock()iflen(q.items)>0{v:=q.items[0]q.items=q.items[1:]q.mu.Unlock()returnv}q.mu.Unlock()// 队列空,啥也没干就继续下一轮 —— 空转烧 CPU}}

这段代码功能没错,但消费者在队列空时会疯狂加锁/解锁空转,一个 goroutine 就能吃满一核。你可能会想到 channel——没错,大多数场景 channel 才是 Go 的首选。但当「等待的条件」比「有没有数据」更复杂(比如「队列长度低于某阈值」「多个条件同时满足」),或者你要一次唤醒全部等待者时,sync.Cond更直接。

sync.Cond 的三个方法

条件变量把「等待」这件事交给运行时调度,等待中的 goroutine 会被挂起、不占 CPU,直到被唤醒:

  • Wait():原子地释放锁并挂起当前 goroutine;被唤醒时自动重新加锁再返回。
  • Signal():唤醒一个等待者(唤醒谁不保证)。
  • Broadcast():唤醒全部等待者。

创建时要绑定一把锁:c := sync.NewCond(&mu)。这把锁保护你在等待/判断时读写的共享状态。

正确写法:队列非空/非满都用条件变量

typeQueuestruct{mu sync.Mutex notEmpty*sync.Cond notFull*sync.Cond items[]intcapint}funcNewQueue(capint)*Queue{q:=&Queue{cap:cap}// 两个条件变量共用同一把锁,保护同一份 itemsq.notEmpty=sync.NewCond(&q.mu)q.notFull=sync.NewCond(&q.mu)returnq}func(q*Queue)Push(vint){q.mu.Lock()deferq.mu.Unlock()forlen(q.items)==q.cap{// 满了就等「非满」信号q.notFull.Wait()}q.items=append(q.items,v)q.notEmpty.Signal()// 放进去了,唤醒一个等着取的消费者}func(q*Queue)Pop()int{q.mu.Lock()deferq.mu.Unlock()forlen(q.items)==0{// 空了就等「非空」信号q.notEmpty.Wait()}v:=q.items[0]q.items=q.items[1:]q.notFull.Signal()// 取走一个,唤醒一个等着放的生产者returnv}

空队列时,Pop里的Wait()会让消费者 goroutine 挂起,完全不占 CPU,直到Push调用Signal()把它叫醒。这就是和忙轮询的本质区别。

坑一:Wait 必须用 for,不能用 if

这是最经典的错误。很多人把上面的for len(q.items) == 0写成if,偶发地就取到空队列 panic 了。

原因有两个:

  1. 虚假唤醒 / 抢跑:Signal唤醒你之后,Wait要重新抢那把锁才能返回。在你抢到锁之前,可能有另一个消费者先抢到锁把数据取走了。等你终于返回时,队列又空了。用if只判断一次就往下走,直接读到空 slice。
  2. for会在被唤醒后重新检查条件,不满足就继续Wait,这才安全。
// 错:被唤醒后不复查,并发下会取到空队列iflen(q.items)==0{q.notEmpty.Wait()}// 对:循环复查条件,这是使用 Cond 的铁律forlen(q.items)==0{q.notEmpty.Wait()}

记死一句话:Cond 的 Wait 永远包在 for 里检查谓词

坑二:调 Wait 前必须持有锁

Wait()内部会先解锁再挂起。如果你没加锁就调Wait,运行时会 panic:sync: unlock of unlocked mutex。所以模式永远是「先LockforWait→ 用完Unlock」。上面例子用defer q.mu.Unlock()保证了这点。

坑三:Signal/Broidcast 怎么选,以及别忘了唤醒

  • 只需让一个等待者继续(比如队列多了一个元素,叫醒一个消费者就够)→Signal(),更省。
  • 状态变化让所有等待者的条件都可能成立(比如「关闭队列,所有人都别等了」)→ 必须Broadcast(),否则只醒一个,其余永远挂着。

关闭场景是重灾区,给个正确收尾:

func(q*Queue)Close(){q.mu.Lock()q.closed=trueq.mu.Unlock()q.notEmpty.Broadcast()// 叫醒所有等待者,让它们看到 closed 后退出q.notFull.Broadcast()}func(q*Queue)Pop()(int,bool){q.mu.Lock()deferq.mu.Unlock()forlen(q.items)==0&&!q.closed{q.notEmpty.Wait()}iflen(q.items)==0{// 只剩 closed 情况return0,false}v:=q.items[0]q.items=q.items[1:]q.notFull.Signal()returnv,true}

关闭时若用Signal只叫醒一个,其他消费者会永久阻塞(goroutine 泄漏)。关闭/广播状态变更一律 Broadcast

什么时候还是该用 channel

别把sync.Cond当默认选择。判断标准:

  • 就是「传数据 + 一对一/多对多收发」→ 用channel,更符合 Go 习惯,还能配select做超时。
  • 「多个 goroutine 等同一个复杂条件,条件成立要一次性叫醒全部」,或者 channel 表达起来别扭(比如状态是一坨结构体字段的组合)→sync.Cond更贴。

一个典型该用 Cond 的例子:一批 worker 都在等「配置加载完成」,加载完Broadcast()一次全放行——用 channel 得靠 close(channel) 技巧,而 Cond 语义更直白。

小结

  • 「等条件成立再继续」别写忙轮询,sync.Cond能让等待的 goroutine 挂起、不烧 CPU。
  • 三个方法:Wait(原子解锁+挂起,唤醒后重新加锁)、Signal(醒一个)、Broadcast(醒全部)。
  • 三条铁律:Wait 必须包在 for 里复查条件调 Wait 前必须持锁状态终结/全员放行用 Broadcast
  • channel 仍是 Go 并发首选;只有「多等待者等复杂条件、需一次唤醒全部」时,Cond 才是更顺手的工具。

一句话记忆:Cond 是「挂起等通知」,for 复查 + 持锁 Wait + 该广播就广播,三样齐了它就稳。

http://www.jsqmd.com/news/1374674/

相关文章:

  • JavaScriptTrainingステージ5:Fetch APIとPromiseを使った非同期処理の究極ガイド
  • 2026AI 一键 P 图网站推荐|ImageGood 网页端,一站式智能图片处理工具 - GrowthUME
  • 如何打造专属AI虚拟助手:5个惊艳功能全面探索
  • 《天道》观后感,循道而行,方得自在
  • Linux硬盘分区管理与文件系统选型实战指南
  • pi-web数据可视化:会话统计与分析的实用工具
  • 整数溢出漏洞解析与CTF实战技巧
  • OBS Studio专业级色彩校正:3D LUT技术深度解析与高级应用指南
  • 2026年伊金霍洛旗变速箱维修保养**:AT/AMT变速箱大修、阀体电脑板维修、打滑漏油异响脱挡闯挡不走过热故障排查与换油清洗服务口碑精选 - 优企名品
  • 2026年小白程序员必看:Agent开发爆发,高薪Offer等你拿!
  • Gmail与Google Docs中Gemini AI助手的关闭与隐藏全攻略
  • web-basics核心组件解析:提升前端开发效率的关键技巧
  • 北京东城区OEM白标贴牌GEO服务商怎么选?2026年靠谱推荐与选型指南 - 企业新闻快传
  • 揭秘AWSIM核心功能:从传感器仿真到ROS2通信全解析
  • C# .NET 周刊 |2026 年 7 月 3 期
  • chatgpt pro版会比plus版更加聪明么?
  • 终极指南:如何让老旧Mac重获新生,完整教程让2012-2017年设备运行最新macOS系统
  • 2025届学术党必备的十大AI科研平台解析与推荐
  • 终极解析!AVP-SLAM-PLUS框架揭秘:从RGB到RGBD模式的核心突破
  • 2026西安本地旅行社推荐|正规靠谱纯玩旅行社怎么选 - 全国旅游攻略
  • 2026年职校学工系统怎么选?高职中职通用采购指南来了
  • 乌鲁木齐直播公会入驻演出资质代办服务靠谱推荐 - 产品推荐官
  • 5个关键功能:英雄联盟智能助手如何彻底改变你的游戏决策方式?
  • ReAct大模型入门:小白也能学会的智能推理与行动策略(收藏版)
  • Python weakref 实战:用弱引用打破循环、做自动失效缓存与观察者
  • AI Token生图成本怎么算?豆包生图中文渲染与大模型API中转站实践
  • 网络安全实战工具全解析:从渗透测试到防御体系
  • 南方防霉洗衣液横评,洗衣液十大品牌潮湿回南天适用款式
  • 深耕商事争议二十载|锦天城彭胜锋律师:法官出身,为企业破解复杂商事困局 - 滚动商讯
  • reverse-engineering-for-beginners中的字符串加密与解密实战技巧