实现同步的两个思路:共享内存、消息通信(内存拷贝),共享内存通常通过锁机制实现,消息通信则基于 [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 的形式。