Golang channel 源码分析

8次阅读

共计 2169 个字符,预计需要花费 6 分钟才能阅读完成。

之前知道 go 团队在实现 channel 这种协程间通信的大杀器时只用了 700 多行代码就解决了,所以就去膜拜读了一把,但之后复盘总觉得多少有点绕,直到有幸找到一个神级 PPT https://speakerdeck.com/kavya… 生动形象的解释了 channel 底层是怎么工作和实现的,于是就带着这篇 PPT 再来复盘一遍 channel 的源码
Hchan 数据结构

初始化
make(chan task, 3) 初始化 channel 在调用方有两种,一种是带缓冲的一种是非缓冲的,其初始化的具体实现除了缓冲非缓冲,还分 channel 的元素是否是指针类型
Send
满足 send 条件下往这个 channel 发送数据的代码, 假设当前没有另一个 goroutine 来接收 channel 的数据
G1:
for task := range tasks {
ch <- task
}

Send to a full channel
当 channel 满了之后 c.qcount > c.dataqsiz 如果还有数据发送到该 channel 则获取当前运行的 goroutine 封装成 sudog, 将其插入 sendq 队列并通知系统将当前 goroutine 停止

此时 hchan 的结构大致长这样
sendq 和 recvq 都是一个由链表实现的 FIFO 队列
这里涉及到三个没见过的东西
1.sudogsudog 是对当前运行的 goroutine 和需要发送数据的封装,有一个前驱指正和后驱指针,hchan 的 sendq 和 recvq 队列则是由 sudog 形成的双向链表

2.goparkunlock —> goparkgopark 将当前 goroutine 置为等待状态
3.goready —> readygoready 将某个 goroutine 唤醒

释放阻塞的 sender goroutine
上面说到,channel 容量已满后, 会阻塞当前 goroutine 并加到发送队列中,那么什么时候会释放这个阻塞的 goroutine 呢。之前看 channel 的学习文章时都说 发送者和接受者必须是成双成对的 (现在理解为一个 gopark, 一个 goready),在下面 channel 的接收端代码中可以看到
因为当从 channel 中接收数据时,如果 sendq 队列上有等待的的 goroutine, 则将它 pop 出来, 执行接收操作(一会儿再讲)后调用 goready 将其唤醒
这里可以看到 虽然 golang 有一句名言叫做“Do not communicate by sharing memory; instead, share memory by communicating.”告诉我们用通信的方式来共享内存而不是用共享内存的方式来通信,在 channel 的内部,接收者和发送者两个 goroutine 却是通过共享 hchan 来实现通信的 (但是发送和接收的数据是通过拷贝来传递的)。
send channel 小结
当 hchan 上没有等待的接收队列 (recvq) 的情况下, 往 channel 发送数据可以总结成以下步骤

hchan 上锁
判断当前 hchan 是否有足够的 buf 空间
如果有,拷贝数据到 buf 中对应的位置
如果 buf 空间不够,或者初始化的是无缓冲 channel, 阻塞当前 goroutine 并将其封装成 sudog 插到 sendq 中等待被接受者唤醒
hchan 解锁

这里只列出了当“hchan 的接收者队列上没有等待的 goroutine”时这种情况,因为在上一句打引号的的情况中有一种之后需要解释的骚操作。
Rcev
channel 的接收实现实质上和发送区别不大, 如果当前没有阻塞等待发送的 goroutine 并且 buf 中有数据, 则从 buf 中将当前 recvx 索引初将需要接收的数据拷贝出来,然后将其在 buf 中清除

Recv from Sender and wakeup Sender
如果在从 channel 接收时,发送队列上有正在阻塞等待的 goroutine, 就是上一节中提到的 send groutine 如何被唤醒的那块内容,拷贝 + 唤醒

Recv from empty channel
如果当前无阻塞等待发送数据的 goroutine, 并且 buf 中没有等待接收的数据,则同 send 一样,将当前的 goroutine, 需要接收的数据指针,封装成 sudog 插入 recvQ 队列尾部, 调用 gopark 停止当前 goroutine

上一节说到,发送端在接收队列中无阻塞等待的 goroutine 时会阻塞并插到 sendq 队列中,并留下了一个悬链说当接收队列上有 goroutine 时会发生一个骚操作。按上面的代码来看,这种情况接受者收到的数据也应该是从 sendq 中取出发送方的 sudog 并将其发送的值拷贝出来,但是在 channel 的实现中,当往一个”空 buf(或者非缓冲)但是接收者队列上有阻塞 goroutine 的”channel 发送数据时,发送方会直接把数据写到接收队列中那个等待接收的 goroutine 中。比起等接收者从 buf 中拷贝数据或者从 sendq 队列中 pop 出 sudog 再拷贝数据,这样做少了一次拷贝的过程

非正常情况下的 sender, recver
未初始化的 channel

往已经关闭的 channel 发送数据

从已关闭的 channel 接受数据

LAST
带着这篇 PPT 来看 channel 的源码感觉一切都一目了然了,反正这篇 PPT 一定要看,而且里面还包含了 channel 在阻塞 goroutine 时 go 调度器运行状态的描述。

正文完
 0