实现同步的两个思路:共享内存、消息通信(内存拷贝),共享内存通常通过锁机制实现,消息通信则基于 [CSP理论](http://www.usingcsp.com/cspbook.pdf)。
go 显示使用 channel ,需要程序员显示的分配消息通道,channel 分为无缓冲的和缓冲两种类型。在了解 channel 内部实现之前,我们先了解 go 语言的调度模型。
go 的调度模型 GPM 模型:
- G - goroutine,Global 队列存放 G
- P - processer,逻辑处理器,P 的 local 队列也存放 G
- M - machine,实际运行 golang 程序作业
M 实际运行要绑定一个 P ,P 类似一个存放资源的队列,P 的 local 队列内存放 G 。M 与 P 绑定,不断从 P 的 local 队列内取 G(无锁操作),切换到 G 的堆栈执行,若没有了 G,则从 Global 队列内取 G (有锁操作),可能会取一批到 P 的 local 队列内,若 Global 内也没有 G ,则尝试从其他 P 中窃取 G ,一般窃取一半,当没有 G 时, M 和 P 解绑,进入休眠。
channel数据结构
src/runtime/chan.go
|
|
waitq 是等待队列和发送队列的包装,包装了当前 goroutine 的指针,即 waitq 指向了一个 goroutine 指针队列,是一个双向队列。初始化带缓冲的 hchan 时:
如上图,初始化一个有 4 个缓冲区的通道,buf 指向一个有界数组,sendx 表示下一次发送数据时要访问 buf 数组的下标,recvx 则表示下一次接收数据时要访问的下标,recvq 保存待接收通道数据的 goroutine 链表,类似的,sendq 负责保存发送数据的 goroutine 。
以此 channel 为例,带有 4 个缓冲区,首先看看 channel 接收数据的流程。
channel接收数据流程
① 协程 G 预往 channel 中插入数据 4,此时 sendx 为 0,表示若要插入数据,将数据放在 buf 下标为 0 处,缓冲区 buf 内空间未满,直接将 G 的数据插入到 buf 内,G 携带的数据放在 buf[0] 处, sendx 加一:

② 第二个协程 G2 携带数据,与第 ① 步类似,buf 空间未满,直接插入,sendx 索引加一:

③ 当插入数据到 buf ,buf空间恰好满了,此时 sendx 归为 0,如此设计让 channel 的缓冲区成为一个 FIFO 类型的通道:

④ 协程 G5 要插入数据,但是 buf 空间已满,则将 G5 插入到发送数据等待队列 sendq,即被阻塞,直到 buf 有空间才从 sendq 取出 G 插入数据:

channel发送数据流程
① 协程 G 预接收数据,recvx 表示 channel 下一个要发送的数据的下标,buf 内有数据,channel 直接将数据发送给 G ,recvx 加一:

② 协程 G2 预接收数据,channel 最后一个数据发送给 G2 , buf 为空,recvx 应归 0:

③ 协程 G3 也要接收 channel 的数据,但是 buf 内已无数据,G3 则被挂起,被放入 recvq 队列内排队:

总结
channel 接收数据时要考虑 buf 是否已满,影响的是 sendx 和 sendq 两个属性,缓冲区满时,协程入 sendq 队列;类似的, channel 发送数据时要考虑 buf 内没有数据的情况,影响 recvx 和 recvq 两个属性。sendx 及 recvx 的变化规律表明 channel 存取数据的方式是先入先出 FIFO 的形式。