上一篇文章主要通过一个现实例子间接反映channel
的一些原理。最后一篇开始介绍一些细节,会涉及到源码。
还是从一个简单的代码程序看起。
我们创建了一个无缓冲channel
,然后往这个channel
发送数据。因为程序中没有读操作ready
,所以发送的时候会阻塞。我们通过汇编代码看它底层的调用。
从图中我们看到,上述发送操作,程序运行时实际调用的runtime.chansend1
。
最终chansend1
最终调用的还是chansend
,chansend
的第三个参数block
是个bool
值,表示操作channel
不能立即成功时是否需要阻塞。
具体哪些操作?
向无缓冲
channel
发送数据且当前无接收者ready
。接收无缓冲
channel
数据且当前无发送者ready
。缓冲
channel
已满,往channel
发送数据。缓冲
channel
为空,接收channel
数据向一个
nil
的channel
发送数据。(注意,向一个nil
的channel
发送数据并不会引发panic
)。向一个
nil
的channel
接收数据。
碰到上面的操作,如果不是特殊处理,我们的应用程序会被阻塞,直到被唤醒。
当然对于向nil
的channel
发送|接收数据,后续再也没机会被唤醒了。
那么如果是快速试错的场景,是不是只要把block
改成false
,在失败的场景下就不会被阻塞了。
编译这段代码。
可以看出,上面这段代码编译后调用selectnbsend
最终发送动作调用的还是chansned
,只是传入的block
是false
。这样一旦操作失败,程序不会被阻塞。
同理我们可以得出接收的调用动作。
到这里我们已经知道,
发送数据,最终调用的
runtime.chansend
。接收数据,最终调用的
runtime.chanrecv
。
接下来我们来说明这两个函数底层是如何操作的。
我们还是以一个无缓冲的channel
和缓冲channel
来说明。
来看一段简单的程序。
值得一提的是,在使用go func
的时候,本质上调用的是runtime.newproc
创建一个g
,然后把这个g
交给调度器调度。
至于什么时候g
被调度,然后执行你的代码逻辑,那就要看调度器的"心情"了。
所以上面创建的两个g
(暂且称为g1和g2),可以看成是我们向调度器提交了两个任务g
,我们无法保证哪个g
会被先调度器调度执行,因此我们也不确定发送和接收这两个操作,谁会先被执行。
假设g1
先被调度器运行,然后执行代码ch<-struct{}{}
。
如果g2
先被调度器运行,然后执行代码<-ch
。
当然我们也可以把上面的代码转化成详细的无缓冲队列核心流程图。
缓冲channel
发送的时候分为三种情况,想想我们上篇文章快递员送快递场景。
如果快递柜未满,直接把快递放入到快递柜。(对应缓冲区未满,把发送数据拷贝到缓冲区)
如果快递柜满了,那快递员只能在那等待快递柜空了。(对应把当前
g
封装成sudog
,然后把sudog放到等待发送消息队列sendq
中,最后挂起当前g
)如果送快递的时候正好客户在那里等,那就直接把快递给他就是了(对应如果发送的时候发现有等待者,直接数据拷贝给他呗)
我们来创建一个例子。
我们创建了一个缓冲区为7的channel
。buffer
就是用来存储缓冲元素的,它实际上是一个环形数组。为什么是环形的?因为这样就可以达到复用空间的效果。
此时没有发送接收动作,所以qcount
为0,发送(sendx
)和接收(recvx
)的位置都为0。
我们来看上面的第一种情况。缓冲区未满,
这块代码就比较简单了。如果缓冲区未满,那就把当前要发送的数据拷贝到缓冲区的发送位置,然后发送位置sendx+1
,然后当前channel
个数qcount+1
,整个流程就结束了。
如果缓冲满的情况下,封装当前g
成sudog
,把这个sudog
入队等待发送队列,最后调用gopark
挂起当前g
,上面无缓冲的时候有提到。
最后一种情况,发送的时候正好有等待接收消息者,那么就从recvq
中拿出最早开始等待的接受者,然后把发送的数据直接拷贝给他。
send
整体有两个动作:拷贝数据----->唤醒等待的recvq
。
那么对于接收操作呢?
快递柜里有我的快递,那我直接拿就行了。(对应缓冲区有数据,根据读
recvx
的位置拿数据)快递柜还没我的快递,但是快递哥打电话说快到了,那我现在楼下转转。(对应缓冲区无数据,把当前
g
封装成sudog
,然后放入到等待接收消息队列recvq
中)。去拿一个快递的时候,正好一个快递员放我另一个快递的时候因为快递柜满了,在那等着。(对应缓冲区满了,且还有等待发送者。此时先到缓冲区获取当前读
recvx
位置的数据,然后再从等待发送者队列中取出最早等待的发送者,把他要发送的数据拷贝拷贝到当前我读取数据的位置(保证先入先出的顺序),最后更新发送位置和更新位置即可)。
第一种情况就简单了。直接通过当前读位置recvx
读取buffer
对应的值,这里还需要通过判断是否忽略返回值,而决定需不需要往当前接收操作拷贝数据。然后移动recvx
位置,元素个数qcount--
,最后解锁即可。
第二种情况,封装当前g
成sudog
,把这个sudog
入队等待接收队列,最后调用gopark
挂起当前g
。上面无缓冲的时候画过这个逻辑。
第三种情况有点复杂。
这种情况下,当获取到一个等待发送者,对于接收者来说,如果我们直接拿它的发送数据返回会发生什么?举个例子,
上图,channel
满了,且sendq
有一个等待发送者(假设是G8
,发送数据为800
),此时执行接收操作,也就出现上述第三种情况。
如果此时我们直接拿G8
的数据,那么数据就不能保证先入先出了。
所以正确的操作是,读取当前recvx
位置(0)buffer
值100
,然后把G8
的数据800拷贝到0的位置,最后把recvq
的位置向前移动,同步发送位置sendx
等于recvq
。这里,可以思考下为啥?
到这里缓冲channe
l的核心流程就说完了。如图,
资料下载
点击下方卡片关注公众号,发送特定关键字获取对应精品资料!
回复「电子书」,获取入门、进阶 Go 语言必看书籍。
回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!
回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。
回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。
回复「后台」,获取后台开发必看 10 本书籍。
对了,看完文章,记得点击下方的卡片。关注我哦~ 👇👇👇
如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!