加载中...

本篇要回答的问题:进程内的执行体(线程与协程)启动起来后如何协同?原子操作、互斥(锁)、同步(等待组/条件变量)、通讯(管道/channel)各自怎么用、各有什么坑?

上一讲介绍了进程、线程、协程三类执行体。既然启动多个执行体,它们就需相互协同。本讲聚焦进程内协同;进程内执行体有两类——用户态协程(以 goroutine 为代表)与操作系统线程。

进程内两类执行体的协同机制概览

原子操作

原子操作是 CPU 提供的能力,与操作系统无关,与执行体种类无关(goroutine 或线程都适用)。每个操作不会中途被打断,原子性由 CPU 保证。

语义上原子操作可用互斥体实现,但快得多。例如 atomic.AddInt32(&val, delta) 等价于一段 Lock/加/Unlock 的互斥代码,但前者更快。

执行体的互斥

互斥体(锁)用于多执行体互斥访问,避免竞争。范式:操作前 Lock、操作后 Unlock

关键判断:"锁慢、channel 快"是误解。误解源于忠告"不要通过共享内存(锁)来通信,要通过通信(channel)来共享内存"。但快慢相对——进程内通讯里比锁快的只有原子操作;channel 本身是共享变量、每个操作都有锁且更耗时。

锁真正的问题:

问题 表现与对策
不容易控制 Lock 后忘 Unlock 是灾难(相当于服务器挂了);doSth 抛异常也会出问题
→ 对策 Go 用 defer 保证配对:defer mutex.Unlock(),写出"对异常安全的代码";无 defer 则需 try…catch 在 catch 中 Unlock 再 throw
锁粒度 锁内若有网络 IO 等费时操作,慢请求几秒会让服务器像挂了
→ 箴言 不要在锁里面执行费时操作("锁里面"指 Lock 与 Unlock 之间)

读写锁

"读多写少"场景应用读写锁:读用 RLock/RUnlock,写用 Lock/Unlock

操作 阻止读 阻止写
读操作 否(读不阻读,可并发)
写操作 是(写阻止一切)

执行体的同步

等待组 WaitGroup

最常见场景:把大任务拆 n 个小任务并行做、等它们一起做完。接口 Add(n) / Done() / Wait()

重要细节:wg.Add(1) 必须在任务 goroutine 还没开始前就调用,否则可能某任务还没开始就被认为结束。

条件变量 Cond

关键判断:条件变量是最通用、设计精巧又极强大的同步原语,强大到连 channel 都能用它实现。接口:NewCond(l Locker) / Broadcast() / Signal() / Wait()

为何 NewCond 要传入锁? 因为 cond.Wait() 内部逻辑是:加入挂起队列 → mutex.Unlock() → 等待被唤醒 → mutex.Lock()

标准使用模板:

mutex.Lock()
defer mutex.Unlock()
for conditionNotMetToDo {   // 必须用 for 不是 if
    cond.Wait()
}
doSomething
if conditionNeedNotify {
    cond.Broadcast()        // 有时可优化为 cond.Signal()
}

关键细节:用 for 而非 if——cond.Wait() 拿到执行权后不代表条件一定满足,要重新判断。

唤醒方式 行为 适用
Broadcast 唤醒该条件变量上所有挂起执行体 最偷懒、一定没问题
Signal 只唤醒其中一个 需满足:①挂起体等待条件一致 ②本次操作释放的资源只够一个执行体做事

Cond 的"变量"是指"一组要在多执行体间协同的数据",“条件"是 Wait 的前置条件与唤醒他人的唤醒条件。本讲用 Cond 亲手实现了一个 Channel(Push/Pop/TryPush/TryPop,借 Cond 等待"队列不满/不空”),不支持无缓冲 channel。

执行体的通讯

管道 Pipe

接口 Pipe() (pr, pw),一个 goroutine 读、一个写。常见用法是读写转换:把一个写入 io.Writer 的算法 Foo,几句话包装成符合 io.ReaderFooReader,从而与接收 io.ReaderBar 串起来。

channel

Go 的 channel 也是管道,只不过是类型安全的管道make(chan Type, n) 创建缓冲为 n 的管道,<-c 读、c <- val 写。

总结

本讲梳理了进程内执行体的协同:原子操作(CPU 能力、最快)、互斥(锁/读写锁)、同步(等待组/条件变量)、通讯(管道/channel)。重点是为锁正名理解条件变量。课本上的信号量未单列,因为它被更强大、性能更好的条件变量取代了。

先把"是什么"回答清楚

概念 一句话说明
原子操作 CPU 保证、不可打断,进程内最快的协同手段
互斥体(锁) 多执行体互斥访问数据,配 defer 保证 Unlock
读写锁 读多写少场景:读不阻读、写阻一切
WaitGroup 等待一组并行任务全部完成(Add 须在 goroutine 前调用)
条件变量 Cond 最通用强大的同步原语,可实现 channel;Wait 内部会解锁/重锁
Broadcast/Signal 唤醒全部 / 唤醒一个
管道/channel 执行体间收发消息,channel 是类型安全的管道

一句话速记

进程内协同 = 原子操作 < 锁 < channel(成本递增);为锁正名(别在锁里干费时操作、用 defer 配对),吃透条件变量(for 循环判断、Wait 内部解锁重锁)就吃透了大半同步原语。

几条值得记住的判断

  • 比锁快的只有原子操作;channel 自带锁、更耗时,"锁慢"是误解。
  • 锁的真问题是难控制 + 粒度:用 defer 配对、绝不在锁里做费时操作
  • 条件变量用 for 而非 if 复检条件;能 Signal 就别滥用 Broadcast。
  • 信号量被条件变量取代——更强大且性能更好。

思考题

本讲用条件变量亲手实现了一个 channel。试着改用信号量实现一遍,再对比分析:为什么说基于"条件变量"的版本更优?顺手把不支持无缓冲 channel(NewChannel(0))的限制也补上。

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关