Channel
2025/8/1大约 4 分钟
Channel底层实现
1. CSP
1.1 核心概念
CSP (Communicating Sequential Processes 通信顺序进程): 消息传递的并发编程模型,由计算机科学家 Tony Hoare于1978年提出。其核心思想是通过通信而非共享内存实现进程间的协作,解决了传统并发模型中锁竞争、死锁等问题。
- 独立运行的顺序进程,通信双方是 同步 且 阻塞 的,即双方互相ready
- 通道分为有无缓冲类型(同/异步)
- 通过算子组合进程,如:
|
(外部选择)、→
(前缀操作)和并行组合||
,形成复杂的并发逻辑
1.2 好处
- 所有权(Ownership):通过发送方在 传递消息 时会 转移数据的所有权
- 避免直接暴露内存地址,根本上消除 数据竞争(Data Race) 和 共享内存 + 锁 易引发的 竟态条件(Race Condition)
- 确定性编程: 同步通信使得 CSP程序的行为更可预测。
- 可验证: 可通过 FDR工具 形式化验证
- 无死锁(Deadlock-free): 系统不会卡死
- 活性(Liveness): 任务最终完成
1.3 对比
并发模型的对比:
特性 | CSP 模型 | Actor 模型 |
---|---|---|
通信方式 | 同步/异步通道: 用"管道"传纸条(可以等对方回复,也可以不等) | 异步邮箱: 往对方的"信箱"里扔纸条(永远不等回复) |
耦合性 | 通道匿名: 只管往管道里扔,不用管谁收 | Actor 具名: 必须知道对方具体地址才能发消息 |
状态管理 | 无共享状态: 数据只在管道里流动,不存本地 | 维护私有状态: 每个Actor都有自己的小本本记数据 |
适用场景 | 高确定性系统: 像红绿灯控制这种要求精确同步的系统 | 分布式系统、容错场景: 像微信聊天这种分布式的、可能出错的场景 |
编程语言实现:
Golang: 则基于模型通过 goroutine 和 channel 进行实现
Occam: 最早的CSP语言,专为并行处理器设计,语法直接对应 CSP 算子
工业场景:
- 分布式系统:▶ 微服务,如 Istio 服务网格控制流量,微服务像快递站(通信),用 channel 当传送带协调包裹(请求)。
- 嵌入式系统: ▶ 自动驾驶, 其传感器数据处理,要求像地铁时刻表一样精确同步。
- 数据处理: ▶ 像工厂流水线,A工序做完扔管道,B工序接着处理,互不阻塞。
2. Channel
Go语言的 Channel 便是基于 CSP 模型的并发编程范式的核心抽象,提倡通过通信而非共享内存的方式来简化并发控制。
其包含了 有/无缓冲的chan 底层通过一把 环形数组+双指针 实现
看起来我们找到了向chan传递数据的银弹——只传指针,然而世界上并没有银弹——
- 传指针相当于上一节说的“共享”数据,很容易带来并发安全问题;
- 对于发送者,传指针给chan很可能会影响逃逸分析,不仅会在堆上分配对象,还会使情况1中的优化失去意义(调用runtime就为了写入一个指针到接收者的栈上)
- 对于接收者来说,操作指针引用的数据需要一次或多次的解引用,而这种解引用很难被优化掉,因此在一些热点代码上很可能会带来可见的性能影响(通常不会有复制数据带来的开销大,但一切得以性能测试为准)。
- 太多的指针会加重gc的负担
使用指针传递时切记要充分考虑上面列出的缺点。
2.1 数据结构
底层是 hchan(高度优化的并发安全队列) 源码如下:
type hchan struct {
buf unsafe.Pointer // 环形缓冲区指针
qcount uint // 当前元素数量
dataqsiz uint // 缓冲区容量
sendx, recvx uint // 读写位置索引
recvq, sendq waitq // 阻塞的接收/发送协程队列
lock mutex // 互斥锁
}
- buf 环形缓冲区: 指向的是一块数组的起始地址,为了给数组初始化长度,故声明 chan 时需要 make 容量大小
- 无缓冲: 声明容量为 0 ,由于无容量,因此发送和接收都会阻塞,见下文阻塞队列
- recv/sendq