Golang channel 源码分析

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

Hchan 数据结构

clipboard.png

初始化

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

Send

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

G1:spa

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中止3d

clipboard.png

clipboard.png

此时hchan的结构大体长这样指针

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

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

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

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调度器运行状态的描述。

相关文章
相关标签/搜索