以前知道go团队在实现channel这种协程间通讯的大杀器时只用了700多行代码就解决了,因此就去膜拜读了一把,但以后复盘总以为多少有点绕,直到有幸找到一个神级PPT https://speakerdeck.com/kavya... 生动形象的解释了channel底层是怎么工做和实现的,因而就带着这篇PPT再来复盘一遍channel的源码golang
make(chan task, 3)
初始化channel在调用方有两种, 一种是带缓冲的一种是非缓冲的,其初始化的具体实现除了缓冲非缓冲,还分channel的元素是不是指针类型数据结构
知足send条件下往这个channel发送数据的代码, 假设当前没有另外一个goroutine来接收channel的数据学习
G1:spa
for task := range tasks { ch <- task }
当channel满了以后 c.qcount > c.dataqsiz 若是还有数据发送到该channel
则获取当前运行的goroutine封装成sudog,将其插入sendq 队列并通知系统将当前goroutine中止3d
此时hchan的结构大体长这样指针
sendq 和 recvq 都是一个由链表实现的FIFO队列code
这里涉及到三个没见过的东西协程
1.sudog
sudog 是对当前运行的goroutine和须要发送数据的封装,有一个前驱指正和后驱指针,hchan的sendq和recvq队列则是由sudog造成的双向链表blog
2.goparkunlock —> gopark
gopark 将当前goroutine置为等待状态索引
3.goready —> ready
goready 将某个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来实现通讯的 (可是发送和接收的数据是经过拷贝来传递的)。
当hchan 上没有等待的接收队列 (recvq) 的状况下, 往channel 发送数据能够总结成如下步骤
这里只列出了当“hchan 的接收者队列上没有等待的goroutine” 时这种状况, 由于在上一句打引号的的状况中有一种以后须要解释的骚操做。
channel 的接收实现实质上和发送区别不大, 若是当前没有阻塞等待发送的goroutine 而且buf中有数据, 则从buf中将当前recvx索引初将须要接收的数据拷贝出来, 而后将其在buf中清除
若是在从channel接收时,发送队列上有正在阻塞等待的goroutine, 就是上一节中提到的send groutine如何被唤醒的那块内容, 拷贝 + 唤醒
若是当前无阻塞等待发送数据的goroutine, 而且buf中没有等待接收的数据, 则同send同样,将当前的goroutine, 须要接收的数据指针,封装成sudog插入recvQ队列尾部, 调用gopark中止当前goroutine
上一节说到, 发送端在接收队列中无阻塞等待的goroutine时会阻塞并插到sendq队列中,并留下了一个悬链说当接收队列上有goroutine时会发生一个骚操做。按上面的代码来看,这种状况接受者收到的数据也应该是从sendq中取出发送方的sudog并将其发送的值拷贝出来,可是在channel的实现中,当往一个 ”空buf(或者非缓冲)可是接收者队列上有阻塞goroutine的” channel发送数据时, 发送方会直接把数据写到接收队列中那个等待接收的goroutine中。比起等接收者从buf中拷贝数据或者从sendq队列中pop出sudog再拷贝数据,这样作少了一次拷贝的过程
带着这篇PPT来看channel的源码感受一切都一目了然了, 反正这篇PPT必定要看,并且里面还包含了channel在阻塞goroutine时 go调度器运行状态的描述。