1. 根本问题:并发程序如何安全地共享数据?
并发的本质矛盾是:多个执行流(goroutine)需要协作,但同时访问共享内存会导致数据竞争。
解决思路有两条:
- 共享内存 + 锁:大家都能访问,但要抢锁(mutex)
- 消息传递:数据的所有权随消息转移,不共享
Go 选择了后者,并将其哲学浓缩为一句话:
“Don’t communicate by sharing memory; share memory by communicating.”
Channel 就是这个哲学的具体实现。
2. 最小模型:channel 需要什么?
从第一性原理出发,一个”安全的数据传递管道”至少需要:
1 | 发送方 ──[数据]──▶ [ 缓冲区? ] ──▶ 接收方 |
| 需求 | 对应机制 |
|---|---|
| 存放数据 | 环形缓冲队列(ring buffer) |
| 多方竞争访问时互斥 | 内部 mutex |
| 没数据时接收方等待 | recvq 等待队列 |
| 缓冲满时发送方等待 | sendq 等待队列 |
| goroutine 的挂起/唤醒 | Go runtime 调度器 |
3. 数据结构:hchan
channel 的底层结构体(runtime/chan.go):
go
1 | type hchan struct { |
环形缓冲区的工作方式:
1 | buf: [ _ | A | B | C | _ | _ ] |
sendx 和 recvx 自动取模,实现循环复用,无需移动数据。
4. 三条核心路径
路径一:发送(ch <- v)
1 | 发送数据 |
路径二:接收(v := <-ch)
1 | 接收数据 |
路径三:关闭(close(ch))
1 | close(ch) |
5. goroutine 的挂起与唤醒
这是 channel 能”阻塞”的关键。Go 用 sudog(sudo goroutine)结构记录等待状态:
go
1 | type sudog struct { |
挂起时:调用 gopark(),将 goroutine 状态从 _Grunning 改为 _Gwaiting,让出 P(处理器),调度器去运行其他 goroutine。
唤醒时:调用 goready(),将 goroutine 重新放入运行队列。
这是用户态调度,不涉及操作系统线程挂起,成本极低。
6. 无缓冲 vs 有缓冲:本质区别
1 | 无缓冲 make(chan T) |
无缓冲 channel 的直接拷贝路径(发送方 → 接收方栈,跳过 buf)是一个重要优化,避免了一次内存分配。
7. select 的实现原理
select 本质是”多路 channel 的竞争等待”:
- 随机打乱 case 顺序(防饥饿)
- 加锁所有涉及的 channel(按地址排序,防死锁)
- 依次检查是否有 case 可以立即执行
- 若都不行,把自己注册到每一个 channel 的等待队列
- 任意一个 channel 就绪后,从其他 channel 的等待队列中移除自己