本篇要回答的问题:进程内的执行体(线程与协程)启动起来后如何协同?原子操作、互斥(锁)、同步(等待组/条件变量)、通讯(管道/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.Reader 的 FooReader,从而与接收 io.Reader 的 Bar 串起来。
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))的限制也补上。

