目录
- 引言
- 场景:短信发送系统
- [方案1:忙轮询 --- CPU 表示"我快烧开了!"](#方案1:忙轮询 — CPU 表示"我快烧开了!" "#%E6%96%B9%E6%A1%881%E5%BF%99%E8%BD%AE%E8%AF%A2--cpu-%E8%A1%A8%E7%A4%BA%E6%88%91%E5%BF%AB%E7%83%A7%E5%BC%80%E4%BA%86")
- [方案2:Sleep 轮询 --- "我眯一会儿,你叫我"](#方案2:Sleep 轮询 — "我眯一会儿,你叫我" "#%E6%96%B9%E6%A1%882sleep-%E8%BD%AE%E8%AF%A2--%E6%88%91%E7%9C%AF%E4%B8%80%E4%BC%9A%E5%84%BF%E4%BD%A0%E5%8F%AB%E6%88%91")
- [方案3:sync.Cond --- "你喊我,我才醒"](#方案3:sync.Cond — "你喊我,我才醒" "#%E6%96%B9%E6%A1%883synccond--%E4%BD%A0%E5%96%8A%E6%88%91%E6%88%91%E6%89%8D%E9%86%92")
- [sync.Cond 的核心概念](#sync.Cond 的核心概念 "#synccond-%E7%9A%84%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5")
- 方案对比分析
- [总结:何时该用 sync.Cond?](#总结:何时该用 sync.Cond? "#%E6%80%BB%E7%BB%93%E4%BD%95%E6%97%B6%E8%AF%A5%E7%94%A8-synccond")
引言
你有没有试过让程序"暂停一下"?不是 time.Sleep(1000) 那种傻等,而是真正优雅地挂起,等我喊你再干活?
如果你曾经用 for { if paused { continue } } 把 CPU 烧到冒烟......别担心,你不是一个人。
今天,我们就用一个"发短信"的小例子,带你从"烧开水"走向"禅意暂停",彻底搞懂 Go 里的 sync.Cond!
场景:短信发送系统
想象你有一个短信平台,能同时开多个 worker 发短信。但老板突然说:"先暂停!等我喝完这杯咖啡再发!"------你得让所有 worker 立刻暂停,等老板说"继续",再接着干活。
听起来简单?我们来看看三种实现方式,从"灾难"到"优雅"的进化之路。
方案1:忙轮询 --- CPU 表示"我快烧开了!"
go
// 方案1:忙轮询(不推荐)
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type SMSManager1 struct {
paused int32 // 0 = running, 1 = paused
tasks chan string
wg sync.WaitGroup
closed bool
mu 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 := 0
for {
sm.mu.Lock()
if sm.closed {
sm.mu.Unlock()
close(sm.tasks)
return
}
paused := atomic.LoadInt32(&sm.paused) == 1
sm.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 = true
sm.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 轮询 --- "我眯一会儿,你叫我"
go
// 方案2:sleep 轮询(推荐简单场景)
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type SMSManager2 struct {
paused int32 // 0 = running, 1 = paused
tasks chan string
wg sync.WaitGroup
closed bool
mu 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):
// 超时后重新检查 paused
continue
}
}
}
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 := 0
for {
sm.mu.Lock()
if sm.closed {
sm.mu.Unlock()
close(sm.tasks)
return
}
paused := atomic.LoadInt32(&sm.paused) == 1
sm.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 = true
sm.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!以下是完整的实现代码:
go
// 方案3:sync.Cond(推荐高并发场景)
package main
import (
"fmt"
"sync"
"time"
)
type SMSManager struct {
mu sync.Mutex
cond *sync.Cond
speed int // 控制速度:0 = 暂停,>0 = 运行
tasks chan string // 任务 channel
wg sync.WaitGroup
closed 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 = speed
needWake := (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 := 0
for {
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 = true
sm.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(),它才会醒来!
而老板(主线程)只需:
go
sm.SetSpeed(10) // 内部调用 sm.cond.Broadcast()
所有暂停的 worker 瞬间醒来,继续干活!零延迟,零浪费,优雅得像瑜伽大师。
sync.Cond 的核心概念
sync.Cond 是 Go 提供的条件变量(Condition Variable),用于 "等待某个条件成立" 的场景。
核心三要素
-
一把锁(通常是 sync.Mutex 或 sync.RWMutex) → cond 必须和这把锁绑定
-
Wait() 方法 → 释放锁 + 挂起 goroutine,直到被唤醒
-
Signal() / Broadcast() 方法
- Signal():唤醒一个等待的 goroutine
- Broadcast():唤醒所有等待的 goroutine(我们用这个)
使用模板
go
// 初始化
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 就是你的救星:
- 需要 "暂停/恢复" 控制(如流控、调试、维护模式)
- 多个 goroutine 等待同一条件成立
- 不想用 channel(比如条件不是"有数据",而是"状态改变")
- 拒绝轮询,追求 零 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 到动态证书加载