🌞

Golang源码阅读 - channel 和 select(一)

「 channel 和 select 」
Go 提供 channel 模型实现同步。

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

go-scheduler.png

channel数据结构

src/runtime/chan.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type hchan struct {
	qcount   uint           // 队列中的数据的量
	dataqsiz uint           // 环形队列的大小
	buf      unsafe.Pointer // 指向 dataqsiz 大小的的数组
	elemsize uint16 // 元素大小
	closed   uint32 // 是否关闭
	elemtype *_type // 元素类型
	sendx    uint   // 发送索引
	recvx    uint   // 接收索引
	recvq    waitq  // 接收列表即 <-chan
	sendq    waitq  // 发送列表即 ch<-

	lock mutex
	// lock保护 hchan 结构体内的所有字段,以及 waitq 内的 sudog,
	// 在持有该锁时,不要改变 G 的状态,因为在栈收缩时会死锁
}


type waitq struct {
	first *sudog
	last  *sudog
}

waitq 是等待队列和发送队列的包装,包装了当前 goroutine 的指针,即 waitq 指向了一个 goroutine 指针队列,是一个双向队列。初始化带缓冲的 hchan 时: channel-select-new.png 如上图,初始化一个有 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 加一: channel-select-G1.png

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

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

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

channel发送数据流程

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

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

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

总结

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

updatedupdated2020-01-152020-01-15