20

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

Hchan 数据结构

clipboard.png

初始化

make(chan task, 3)
clipboard.png
初始化channel在调用方有两种, 一种是带缓冲的一种是非缓冲的,其初始化的具体实现除了缓冲非缓冲,还分channel的元素是否是指针类型
clipboard.png

Send

满足send条件下往这个channel发送数据的代码, 假设当前没有另一个goroutine来接收channel的数据

G1:

for task := range tasks {
    ch <- task
}

clipboard.png
clipboard.png

Send to a full channel

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

clipboard.png

clipboard.png

此时hchan的结构大致长这样

clipboard.png
sendq 和 recvq 都是一个由链表实现的FIFO队列

这里涉及到三个没见过的东西

1.sudog
sudog 是对当前运行的goroutine和需要发送数据的封装,有一个前驱指正和后驱指针,hchan的sendq和recvq队列则是由sudog形成的双向链表

clipboard.png

2.goparkunlock —> gopark
gopark 将当前goroutine置为等待状态
clipboard.png

3.goready —> ready
goready 将某个goroutine 唤醒
clipboard.png

clipboard.png

释放阻塞的sender goroutine

clipboard.png
上面说到,channel容量已满后, 会阻塞当前goroutine并加到发送队列中, 那么什么时候会释放这个阻塞的goroutine呢。 之前看channel的学习文章时都说 发送者和接受者必须是成双成对的 (现在理解为一个gopark, 一个goready),在下面channel的接收端代码中可以看到
clipboard.png

因为当从channel中接收数据时, 如果sendq队列上有等待的的goroutine, 则将它pop出来, 执行接收操作(一会儿再讲)后调用goready将其唤醒
clipboard.png

这里可以看到 虽然 golang 有一句名言叫做 “Do not communicate by sharing memory; instead, share memory by communicating.” 告诉我们用通信的方式来共享内存而不是用共享内存的方式来通信,在channel的内部, 接收者和发送者两个goroutine却是通过共享hchan来实现通信的 (但是发送和接收的数据是通过拷贝来传递的)。

send channel 小结

当hchan 上没有等待的接收队列 (recvq) 的情况下, 往channel 发送数据可以总结成以下步骤

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

这里只列出了当“hchan 的接收者队列上没有等待的goroutine” 时这种情况, 因为在上一句打引号的的情况中有一种之后需要解释的骚操作。

Rcev

channel 的接收实现实质上和发送区别不大, 如果当前没有阻塞等待发送的goroutine 并且buf中有数据, 则从buf中将当前recvx索引初将需要接收的数据拷贝出来, 然后将其在buf中清除

clipboard.png

Recv from Sender and wakeup Sender

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

clipboard.png

Recv from empty channel

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

clipboard.png

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

clipboard.png

clipboard.png

clipboard.png

非正常情况下的sender, recver

未初始化的channel

clipboard.png

往已经关闭的channel发送数据

clipboard.png

从已关闭的channel接受数据

clipboard.png

LAST

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


薛薛薛
515 声望47 粉丝

专业 键盘手🎹 吉他手🎸