golang并发模型基于通信顺序进程(Communicating sequential processes,CSP)实现。goroutine和channel分别对应CSP中的实体和传递信息的媒介,goroutine之间会通过channel传递数据。
目前的channel收发操作也遵循先进先出的设计(1.6版本之后)
具体规则:
- 先从channel读取数据的Goroutine先接收数据;
- 先向channel发送数据的Goroutine先发送数据;
channel在实现上是个有锁队列。
select可以通过default语句实现非阻塞的channel发送和接收,此时在代码中体现是chansend
和chanrecv
的block
参数。
1.select实现
1.1.select行为
- select不存在任何的case:直接block
- select只存在一个case:channel为nil时阻塞,否则行为回退至
x, ok = <-c
- select存在两个case,其中一个是default:非阻塞发送/接收,若失败则走default语句
- select存在多个case:若存在多个ready的channel,随机执行,否则block到可以执行为止
1.2.实现原理
select
语句在编译期间会被转换成OSELECT
节点。每个OSELECT
节点对应一组OCASE
节点,若OCASE
的执行条件是空,那就意味着这是一个default
节点。
编译器在中间代码生成期间会根据select
中case
的不同对控制语句进行优化,具体代码在gc.walkselectcases
函数中
默认情况下,调用selectgo
函数获取可执行的scase
结构体。
selectgo
函数首先会进行执行必要的初始化操作并决定处理case
的两个顺序,轮询顺序pollOrder
和加锁顺序lockOrder
(两个数组,用于后续的case
遍历):
轮询顺序:通过runtime.fastrandn
函数引入随机性;
加锁顺序:按照channel
的地址排序后确定加锁顺序;
scase
结构体包含channel
的指针,因此在轮询过程中,可以直接访问hchan
结构体获取channel
的发送接收状态,并进行接收或者发送(逻辑基本上是重写chansend
和chanrecv
)。
1 | type scase struct { |
2.channel数据结构
sendq和recvq至少有一个为空(除非单个goroutine同时发送接收到一个缓冲channel)
对于带缓冲的channel,qcount > 0
代表recvq为空,qcount < dataqsiz
代表sendq为空.
1 | type hchan struct { |
3.channel发送数据
数据发送通过chansend
函数实现,分为三种情况:
- 存在recvq:当存在等待的接收者时,通过
send
直接将数据发送给阻塞的接收者; - 发送至缓冲区:当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区;
- 发送阻塞:当不存在缓冲区或者缓冲区已满时,等待其他 Goroutine 从 Channel 接收数据;
3.1.存在recvq
如果recvq存在,且channel未关闭,chansend函数会从recvq取最先等待的goroutine并发送数据。
发送数据之后,send函数会调用goready(gp, skip+1)
,标记goroutine为_Grunnable
,但不立即执行。
1 | if sg := c.recvq.dequeue(); sg != nil { |
3.2.发送至缓冲区
通过chanbuf
计算下一个位置的指针,然后调用typedmemmove
将发送的数据复制至缓冲区中,并增加sendx
和qcount
。
1 | if c.qcount < c.dataqsiz { |
3.3.发送阻塞
- 调用
getg
获取发送数据使用的goroutine; - 执行
acquireSudog
获取sudog
,并设置阻塞发送的相关信息,例如发送的channel、是否在select中和待发送数据的内存地址等; - 将
sudog
实例加入发送等待队列,并设置goroutine的parkingOnChan
属性; - 调用
goparkunlock
将当前的goroutine陷入等待; - 被调度器唤醒后,说明数据已经发送成功,清理goroutine属性,并释放
sudog
结构体;
4.channel接收数据
接收数据通过chanrecv
函数实现,情况和发送类似:
- 存在sendq:当存在等待的发送者时,通过
recv
从阻塞的发送者或者缓冲区中获取数据; - 从缓冲区接收:当缓冲区存在数据时,从channel的缓冲区中接收数据;
- 接收阻塞:当缓冲区中不存在数据时,等待其他goroutine向channel发送数据;
4.1.存在sendq
存在sendq和存在recvq的在接收发送阶段的行为会有点区别:发送时存在recvq,缓冲区一定不存在数据;接收时,sendq的存在表示缓冲区数据时满的。
这个区别导致recv
函数的实现会比send
更复杂。
recv
函数区分了循环队列大小(dataqsiz
)大小是否为0的情况。
无缓冲区(dataqsiz==0
)时,直接从sendq
的出队sudog中获取数据;否则,将缓冲区头数据出队,并将sendq
中的对应元素入队(考虑到缓冲区队列此刻是满的,因此操作的是一个位置,重点是需要修改recvx
和sendx
)。
接收之后,需要将sudog中对应的goroutine通过goready(gp, skip+1)
唤醒(但不立刻调度)。
4.2.从缓冲区接收
通过chanbuf
获取缓冲区指针之后,复制数据,然后修改recvx
和qcount
,并解锁lock
。
此时不涉及发送相关的数据,因此不需要修改sendx
。
4.3.接收阻塞
和发送的阻塞操作一致,入队并阻塞,等待唤醒。