上一篇文章主要通过一个现实例子间接反映​​channel​​的一些原理。最后一篇开始介绍一些细节,会涉及到源码。

还是从一个简单的代码程序看起。

彻底搞懂channel原理(三)_编程语言

我们创建了一个无缓冲​​channel​​​,然后往这个​​channel​​​发送数据。因为程序中没有读操作​​ready​​,所以发送的时候会阻塞。我们通过汇编代码看它底层的调用。

彻底搞懂channel原理(三)_java_02

从图中我们看到,上述发送操作,程序运行时实际调用的​​runtime.chansend1​​。

彻底搞懂channel原理(三)_多线程_03

最终​​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​​,在失败的场景下就不会被阻塞了。

编译这段代码。

彻底搞懂channel原理(三)_python_04

彻底搞懂channel原理(三)_多线程_05

彻底搞懂channel原理(三)_python_06

可以看出,上面这段代码编译后调用​​selectnbsend​​​最终发送动作调用的还是​​chansned​​​,只是传入的​​block​​​是​​false​​。这样一旦操作失败,程序不会被阻塞。

同理我们可以得出接收的调用动作。

彻底搞懂channel原理(三)_队列_07

彻底搞懂channel原理(三)_python_08

彻底搞懂channel原理(三)_多线程_09

彻底搞懂channel原理(三)_python_10

到这里我们已经知道,

  • 发送数据,最终调用的​​runtime.chansend​​。
  • 接收数据,最终调用的​​runtime.chanrecv​​。

接下来我们来说明这两个函数底层是如何操作的。

我们还是以一个无缓冲的​​channel​​​和缓冲​​channel​​来说明。

来看一段简单的程序。

彻底搞懂channel原理(三)_编程语言_11

值得一提的是,在使用​​go func​​​的时候,本质上调用的是​​runtime.newproc​​​创建一个​​g​​​,然后把这个​​g​​交给调度器调度。

至于什么时候​​g​​被调度,然后执行你的代码逻辑,那就要看调度器的"心情"了。

所以上面创建的两个​​g​​​(暂且称为g1和g2),可以看成是我们向调度器提交了两个任务​​g​​​,我们无法保证哪个​​g​​会被先调度器调度执行,因此我们也不确定发送和接收这两个操作,谁会先被执行。

假设​​g1​​​先被调度器运行,然后执行代码​​ch<-struct{}{}​​。

彻底搞懂channel原理(三)_队列_12

如果​​g2​​​先被调度器运行,然后执行代码​​<-ch​​。

彻底搞懂channel原理(三)_多线程_13

当然我们也可以把上面的代码转化成详细的无缓冲队列核心流程图。

彻底搞懂channel原理(三)_队列_14

缓冲​​channel​​发送的时候分为三种情况,想想我们上篇文章快递员送快递场景。

  • 如果快递柜未满,直接把快递放入到快递柜。(对应缓冲区未满,把发送数据拷贝到缓冲区)
  • 如果快递柜满了,那快递员只能在那等待快递柜空了。(对应把当前​​g​​​封装成​​sudog​​​,然后把sudog放到等待发送消息队列​​sendq​​​中,最后挂起当前​​g​​)
  • 如果送快递的时候正好客户在那里等,那就直接把快递给他就是了(对应如果发送的时候发现有等待者,直接数据拷贝给他呗)

我们来创建一个例子。

彻底搞懂channel原理(三)_多线程_15

我们创建了一个缓冲区为7的​​channel​​​。​​buffer​​就是用来存储缓冲元素的,它实际上是一个环形数组。为什么是环形的?因为这样就可以达到复用空间的效果。

此时没有发送接收动作,所以​​qcount​​​为0,发送(​​sendx​​​)和接收(​​recvx​​)的位置都为0。

我们来看上面的第一种情况。缓冲区未满,

彻底搞懂channel原理(三)_多线程_16

这块代码就比较简单了。如果缓冲区未满,那就把当前要发送的数据拷贝到缓冲区的发送位置,然后发送位置​​sendx+1​​​,然后当前​​channel​​​个数​​qcount+1​​,整个流程就结束了。

如果缓冲满的情况下,封装当前​​g​​​成​​sudog​​​,把这个​​sudog​​​入队等待发送队列,最后调用​​gopark​​​挂起当前​​g​​,上面无缓冲的时候有提到。

最后一种情况,发送的时候正好有等待接收消息者,那么就从​​recvq​​中拿出最早开始等待的接受者,然后把发送的数据直接拷贝给他。

彻底搞懂channel原理(三)_队列_17

彻底搞懂channel原理(三)_python_18

​send​​​整体有两个动作:拷贝数据----->唤醒等待的​​recvq​​。

那么对于接收操作呢?

  • 快递柜里有我的快递,那我直接拿就行了。(对应缓冲区有数据,根据读​​recvx​​的位置拿数据)
  • 快递柜还没我的快递,但是快递哥打电话说快到了,那我现在楼下转转。(对应缓冲区无数据,把当前​​g​​​封装成​​sudog​​​,然后放入到等待接收消息队列​​recvq​​中)。
  • 去拿一个快递的时候,正好一个快递员放我另一个快递的时候因为快递柜满了,在那等着。(对应缓冲区满了,且还有等待发送者。此时先到缓冲区获取当前读​​recvx​​位置的数据,然后再从等待发送者队列中取出最早等待的发送者,把他要发送的数据拷贝拷贝到当前我读取数据的位置(保证先入先出的顺序),最后更新发送位置和更新位置即可)。

第一种情况就简单了。直接通过当前读位置​​recvx​​​读取​​buffer​​​对应的值,这里还需要通过判断是否忽略返回值,而决定需不需要往当前接收操作拷贝数据。然后移动​​recvx​​​位置,元素个数​​qcount--​​ ,最后解锁即可。

彻底搞懂channel原理(三)_编程语言_19

第二种情况,封装当前​​g​​​成​​sudog​​​,把这个​​sudog​​​入队等待接收队列,最后调用​​gopark​​​挂起当前​​g​​。上面无缓冲的时候画过这个逻辑。

第三种情况有点复杂。

彻底搞懂channel原理(三)_编程语言_20

这种情况下,当获取到一个等待发送者,对于接收者来说,如果我们直接拿它的发送数据返回会发生什么?举个例子,

彻底搞懂channel原理(三)_编程语言_21

上图,​​channel​​​满了,且​​sendq​​​有一个等待发送者(假设是​​G8​​​,发送数据为​​800​​),此时执行接收操作,也就出现上述第三种情况。

如果此时我们直接拿​​G8​​的数据,那么数据就不能保证先入先出了。

所以正确的操作是,读取当前​​recvx​​​位置(0)​​buffer​​​值​​100​​​,然后把​​G8​​​的数据800拷贝到0的位置,最后把​​recvq​​​的位置向前移动,同步发送位置​​sendx​​​等于​​recvq​​。这里,可以思考下为啥?

到这里缓冲​​channe​​l的核心流程就说完了。如图,

彻底搞懂channel原理(三)_java_22


如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!