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

从“CPU 烧开水“到优雅暂停:Go 里 sync.Cond 的正确打开方式

目录

  • 引言
  • 场景:短信发送系统
  • 方案1:忙轮询 — CPU 表示"我快烧开了!"
  • 方案2:Sleep 轮询 — “我眯一会儿,你叫我”
  • 方案3:sync.Cond — “你喊我,我才醒”
  • sync.Cond 的核心概念
  • 方案对比分析
  • 总结:何时该用 sync.Cond?

引言

你有没有试过让程序"暂停一下"?不是 time.Sleep(1000) 那种傻等,而是真正优雅地挂起,等我喊你再干活?

如果你曾经用 for { if paused { continue } } 把 CPU 烧到冒烟……别担心,你不是一个人。

今天,我们就用一个"发短信"的小例子,带你从"烧开水"走向"禅意暂停",彻底搞懂 Go 里的 sync.Cond

场景:短信发送系统

想象你有一个短信平台,能同时开多个 worker 发短信。但老板突然说:“先暂停!等我喝完这杯咖啡再发!”——你得让所有 worker 立刻暂停,等老板说"继续",再接着干活。

听起来简单?我们来看看三种实现方式,从"灾难"到"优雅"的进化之路。

方案1:忙轮询 — CPU 表示"我快烧开了!"

// 方案1:忙轮询(不推荐)
package mainimport ("fmt""sync""sync/atomic""time"
)type SMSManager1 struct {paused int32 // 0 = running, 1 = pausedtasks  chan stringwg     sync.WaitGroupclosed boolmu     sync.Mutex
}func NewSMSManager1() *SMSManager1 {return &SMSManager1{tasks: make(chan string, 100),}
}func (sm *SMSManager1) SetSpeed(speed int) {if speed == 0 {atomic.StoreInt32(&sm.paused, 1)} else {atomic.StoreInt32(&sm.paused, 0)}
}func (sm *SMSManager1) worker(id int) {defer sm.wg.Done()for {sm.mu.Lock()if sm.closed {sm.mu.Unlock()return}sm.mu.Unlock()if atomic.LoadInt32(&sm.paused) == 1 {// 忙轮询!CPU 飙升continue}select {case task, ok := <-sm.tasks:if !ok {return}fmt.Printf("Worker %d: sending %s\n", id, task)time.Sleep(50 * time.Millisecond) // 模拟发送default:time.Sleep(1 * time.Millisecond) // 减轻一点,但仍是轮询}}
}func (sm *SMSManager1) StartWorkers(n int) {sm.wg.Add(n)for i := 0; i < n; i++ {go sm.worker(i)}go sm.producer()
}func (sm *SMSManager1) producer() {taskID := 0for {sm.mu.Lock()if sm.closed {sm.mu.Unlock()close(sm.tasks)return}paused := atomic.LoadInt32(&sm.paused) == 1sm.mu.Unlock()if !paused {taskID++select {case sm.tasks <- fmt.Sprintf("Task-%d", taskID):default:// 丢弃或阻塞,这里丢弃}time.Sleep(200 * time.Millisecond)} else {time.Sleep(10 * time.Millisecond)}}
}func (sm *SMSManager1) Stop() {sm.mu.Lock()sm.closed = truesm.mu.Unlock()close(sm.tasks)sm.wg.Wait()
}func main() {fmt.Println("=== 方案1:忙轮询(不推荐)===")sm := NewSMSManager1()sm.SetSpeed(1)sm.StartWorkers(3)time.Sleep(2 * time.Second)fmt.Println(">>> Pause (speed=0)")sm.SetSpeed(0)time.Sleep(3 * time.Second)fmt.Println(">>> Resume (speed=1)")sm.SetSpeed(1)time.Sleep(2 * time.Second)fmt.Println(">>> Stopping")sm.Stop()
}

这就是传说中的 “忙轮询”(Busy Waiting)。

worker 一旦发现暂停,就疯狂 continue,CPU 核心瞬间飙到 100%。你的笔记本风扇开始怒吼,隔壁同事以为你在挖矿。

问题所在

  • 没有"等待"机制,goroutine 一直在跑,浪费资源,毫无优雅可言
  • 即使加上短暂的 sleep,仍然是在浪费 CPU 周期
  • 响应速度虽然快,但代价太大

方案2:Sleep 轮询 — “我眯一会儿,你叫我”

// 方案2:sleep 轮询(推荐简单场景)
package mainimport ("fmt""sync""sync/atomic""time"
)type SMSManager2 struct {paused int32 // 0 = running, 1 = pausedtasks  chan stringwg     sync.WaitGroupclosed boolmu     sync.Mutex
}func NewSMSManager3() *SMSManager2 {return &SMSManager2{tasks: make(chan string, 100),}
}func (sm *SMSManager2) SetSpeed(speed int) {if speed == 0 {atomic.StoreInt32(&sm.paused, 1)} else {atomic.StoreInt32(&sm.paused, 0)}
}func (sm *SMSManager2) worker(id int) {defer sm.wg.Done()for {// 检查关闭sm.mu.Lock()if sm.closed {sm.mu.Unlock()return}sm.mu.Unlock()// 检查暂停if atomic.LoadInt32(&sm.paused) == 1 {time.Sleep(50 * time.Millisecond) // 低频轮询continue}// 尝试读取任务,带超时select {case task, ok := <-sm.tasks:if !ok {return}fmt.Printf("Worker %d: sending %s\n", id, task)time.Sleep(50 * time.Millisecond)case <-time.After(100 * time.Millisecond):// 超时后重新检查 pausedcontinue}}
}func (sm *SMSManager2) StartWorkers(n int) {sm.wg.Add(n)for i := 0; i < n; i++ {go sm.worker(i)}go sm.producer()
}func (sm *SMSManager2) producer() {taskID := 0for {sm.mu.Lock()if sm.closed {sm.mu.Unlock()close(sm.tasks)return}paused := atomic.LoadInt32(&sm.paused) == 1sm.mu.Unlock()if paused {time.Sleep(50 * time.Millisecond)continue}taskID++sm.tasks <- fmt.Sprintf("Task-%d", taskID)time.Sleep(200 * time.Millisecond)}
}func (sm *SMSManager2) Stop() {sm.mu.Lock()sm.closed = truesm.mu.Unlock()close(sm.tasks)sm.wg.Wait()
}func main() {fmt.Println("=== 方案2:sleep 轮询(推荐简单场景)===")sm := NewSMSManager3()sm.SetSpeed(1)sm.StartWorkers(3)time.Sleep(2 * time.Second)fmt.Println(">>> Pause (speed=0)")sm.SetSpeed(0)time.Sleep(3 * time.Second)fmt.Println(">>> Resume (speed=1)")sm.SetSpeed(1)time.Sleep(2 * time.Second)fmt.Println(">>> Stopping")sm.Stop()
}

好一点了!至少不烧 CPU 了。但问题来了:50ms 是拍脑袋定的。

  • 太短?还是有点浪费
  • 太长?老板喊"继续"后,worker 还在梦里,延迟高

适用场景

简单脚本、临时 demo 可以凑合用,但不是真正的"即时响应"。

方案3:sync.Cond — “你喊我,我才醒”

终于,主角登场:sync.Cond!以下是完整的实现代码:

// 方案3:sync.Cond(推荐高并发场景)
package mainimport ("fmt""sync""time"
)type SMSManager struct {mu     sync.Mutexcond   *sync.Condspeed  int          // 控制速度:0 = 暂停,>0 = 运行tasks  chan string  // 任务 channelwg     sync.WaitGroupclosed bool
}func NewSMSManager() *SMSManager {sm := &SMSManager{tasks: make(chan string, 100),}sm.cond = sync.NewCond(&sm.mu) // 关联互斥锁return sm
}// 设置速度(线程安全)
func (sm *SMSManager) SetSpeed(speed int) {sm.mu.Lock()wasPaused := (sm.speed == 0)sm.speed = speedneedWake := (speed > 0 && wasPaused) // 从暂停变为运行sm.mu.Unlock()if needWake {sm.cond.Broadcast() // 唤醒所有等待的 goroutine}
}// 启动工作池
func (sm *SMSManager) StartWorkers(workerCount int) {sm.wg.Add(workerCount)for i := 0; i < workerCount; i++ {go sm.worker(i)}// 启动生产者(模拟)go sm.producer()
}// 工作者:从 channel 读任务,但受 speed 控制
func (sm *SMSManager) worker(id int) {defer sm.wg.Done()for {// 1. 先检查是否要暂停sm.mu.Lock()for sm.speed == 0 && !sm.closed {sm.cond.Wait() // 挂起,直到被 Broadcast 唤醒}if sm.closed {sm.mu.Unlock()return}sm.mu.Unlock()// 2. 尝试读取任务(带超时防永久阻塞)select {case task, ok := <-sm.tasks:if !ok {return}fmt.Printf("Worker %d sending SMS: %s\n", id, task)time.Sleep(100 * time.Millisecond) // 模拟发送耗时case <-time.After(1 * time.Second):// 防止在 tasks 阻塞时无法响应 speed=0}}
}// 模拟生产者
func (sm *SMSManager) producer() {taskID := 0for {sm.mu.Lock()if sm.closed {sm.mu.Unlock()close(sm.tasks)return}// 如果暂停,生产者也应暂停for sm.speed == 0 {sm.cond.Wait()}sm.mu.Unlock()taskID++select {case sm.tasks <- fmt.Sprintf("Task-%d", taskID):case <-time.After(5 * time.Second):// 超时退出(仅 demo)return}time.Sleep(time.Duration(200/sm.speed) * time.Millisecond) // 简单限速}
}// 停止整个系统
func (sm *SMSManager) Stop() {sm.mu.Lock()sm.closed = truesm.mu.Unlock()sm.cond.Broadcast() // 唤醒所有等待者,让它们退出sm.wg.Wait()
}// ===== 使用示例 =====
func main() {sm := NewSMSManager()sm.SetSpeed(5) // 初始速度 >0,开始工作sm.StartWorkers(3)time.Sleep(3 * time.Second)fmt.Println(">>> Pausing (speed=0)")sm.SetSpeed(0)time.Sleep(5 * time.Second)fmt.Println(">>> Resuming (speed=10)")sm.SetSpeed(10)time.Sleep(3 * time.Second)fmt.Println(">>> Stopping")sm.Stop()
}

当 worker 发现 speed == 0(暂停),它会:

  • 调用 cond.Wait()
  • 自动释放锁
  • 进入睡眠状态,不消耗 CPU
  • 直到有人调用 cond.Broadcast() 或 cond.Signal(),它才会醒来!

而老板(主线程)只需:

sm.SetSpeed(10) // 内部调用 sm.cond.Broadcast()

所有暂停的 worker 瞬间醒来,继续干活!零延迟,零浪费,优雅得像瑜伽大师。

sync.Cond 的核心概念

sync.Cond 是 Go 提供的条件变量(Condition Variable),用于 “等待某个条件成立” 的场景。

核心三要素

  1. 一把锁(通常是 sync.Mutex 或 sync.RWMutex)
    → cond 必须和这把锁绑定

  2. Wait() 方法
    → 释放锁 + 挂起 goroutine,直到被唤醒

  3. Signal() / Broadcast() 方法

    • Signal():唤醒一个等待的 goroutine
    • Broadcast():唤醒所有等待的 goroutine(我们用这个)

使用模板

// 初始化
mu := &sync.Mutex{}
cond := sync.NewCond(mu)// 等待方
mu.Lock()
for !condition { // 必须用 for,防止"虚假唤醒"cond.Wait()
}
// 条件满足,干活
mu.Unlock()// 通知方
mu.Lock()
condition = true
cond.Broadcast() // 或 Signal()
mu.Unlock()

重要提醒

  • Wait() 必须在持有锁的情况下调用
  • 条件判断必须用 for 循环,不能用 if(防止虚假唤醒)
  • 唤醒后要重新检查条件,因为可能被其他 goroutine 抢先

方案对比分析

方案CPU 消耗响应速度代码复杂度适用场景
忙轮询🔥 极高❌ 别用
Sleep 轮询🟢 低慢(有延迟)✅ 简单场景
sync.Cond🟢 几乎为零⚡ 即时✅ 高并发、需精确控制

在我们的短信系统中:

  • 暂停时:worker 真正"挂起",不占资源
  • 恢复时:所有 worker 瞬间响应,无缝继续
  • 关闭时:通过 closed 标志 + Broadcast() 安全退出

总结:何时该用 sync.Cond?

当你遇到以下场景,sync.Cond 就是你的救星:

  1. 需要 “暂停/恢复” 控制(如流控、调试、维护模式)
  2. 多个 goroutine 等待同一条件成立
  3. 不想用 channel(比如条件不是"有数据",而是"状态改变")
  4. 拒绝轮询,追求 零 CPU 浪费 + 即时响应

记住

sync.Cond 不是万能的,但在"状态等待"场景下,它比 channel 更直接,比轮询更优雅。

最后:

优雅的程序,从学会"等待"开始。
别再让 goroutine 在梦里狂奔了,给它一个 sync.Cond,让它安心睡觉,等你一声令下,再奋起直追 💪

往期部分文章列表

  • 时移世易,篡改天机:吾以 Go 语令 Windows 文件“返老还童“记
  • golang圆阵列图记:天灵灵地灵灵图标排圆形
  • golang解图记
  • 从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
  • 用 Go 手搓一个内网 DNS 服务器:从此告别 IP 地址,用域名畅游家庭网络!
  • 我用Go写了个华容道游戏,曹操终于不用再求关羽了!
  • 用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法
  • 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
  • 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
  • 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
  • Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
  • 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
  • 验证码识别API:告别收费接口,迎接免费午餐
  • 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
  • 无奈!我用go写了个MySQL服务
  • 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
  • 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
  • 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
  • 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
  • 用 Go 语言实现《周易》大衍筮法起卦程序
  • Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
  • 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
  • Golang + OpenSSL 实现 TLS 安全通信:从私有 CA 到动态证书加载
http://www.dtcms.com/a/560865.html

相关文章:

  • 大模型系列——Excel数据治理新思路:引入智能体实现自动纠错【Python+Agent】
  • Pyppeteer 使用教程
  • React性能优化:useMemo vs useCallback
  • Onsemi展示了垂直GaN-on-GaN半导体
  • 专业机票网站建设禅城区建设局网站
  • Java 日志演进:一文读懂主流框架
  • 第3章 变量与数据类型
  • pyside6 qt 事件循环
  • Secertpad搭建
  • 吞吐量、延迟、内存:深入理解垃圾回收的“三元悖论”
  • List接口和常用方法
  • 计算机一级考试网站怎么做用织梦系统做网站产权
  • Java 数据结构第二十八期:反射、枚举以及 lambda 表达式
  • Linux 磁盘分区与系统监控完全指南
  • 是普通网站地图好还是rss地图好一点网站建设申请费用
  • 使用Graphics2D创建滑块验证码
  • Flutter provide框架内部实现原理刨析
  • 关于rpm,yum,apt
  • 15.6.Bat脚本编写
  • 景德镇网站制作广告法
  • 可以做头像的网站网站区域名是什么
  • 新手记录使用uniapp-x开发鸿蒙应用
  • Linux+Apache+MySQL+PHP 架构下搭建 Discuz 社区论坛
  • 可替代Github Copilot的插件分享CodeGeeX
  • Ubuntu学习笔记
  • 双非大学生自学鸿蒙5.0零基础入门到项目实战 - 歌曲列表
  • 双非大学生自学鸿蒙5.0零基础入门到项目实战 -ArkTs核心
  • UVa 10989 Bomb Divide and Conquer
  • 【Linux】版本控制器Git和调试器—gdb/cgdb的使用
  • 怎么把个人做的网站上传到网上wordpress用户名钩子