之前知道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.sudog
sudog 是对当前运行的goroutine和需要发送数据的封装,有一个前驱指正和后驱指针,hchan的sendq和recvq队列则是由sudog形成的双向链表
2.goparkunlock —> gopark
gopark 将当前goroutine置为等待状态
3.goready —> ready
goready 将某个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调度器运行状态的描述。
有疑问加站长微信联系(非本文作者)