golang 内置的 channel 数据结构是什么鬼样子呢?

Updated on with 0 views and 0 comments


本文基于go1.14.12版本,channel是go语言中的一大特色,它的用途是用来在goroutine之间传输数据,这里你可能要问了,为什么一定得是goroutine之间传输数据呢,函数之间传递不行吗?

因为正常的传输数据直接以参数的形式传递就可以了,只有在并发场景当中,多个线程彼此隔离的情况下,才需要一个特殊的结构传输数据。

废话不多说,channel的数据结构是:

定义个一个channel的时候,会生成一个hchan的数据结构:

  • qcount:表示当前buf中有多少个数据

  • dataqsize:也就是buffer size,最终会体现到buf的大小里

  • buf:起始就是一个环形数组

  • sendx:环形数组索引字段,发送方往channel里填入数据的位置,当buf满了之后,又回到0

  • recvx:环形数组索引字段,接收方从channel里取数据的位置,当buf满了之后,又回到0

  • sendq:如果buf满了,然后又有更多的发送方发送过来数据,就会把发送方的* goroutine先挂在sendq的队列上阻塞起来

  • recvq:如果buf空了,而且也没有阻塞的sendq,那么就会把接收方的goroutine挂在recvq上阻塞起来

  • lock:保护channel所有字段的锁

当我们往channel里面写数据的时候,channel里面这个小社会是咋这个运行的的?

当我们定义一个没有缓冲区的channel的时候:buffer size = 0

当我们定义一个有缓冲区的channel的时候:buffer size=3

往channel里发送数据:

当qcount == dataqsize 也就是说 buf已经满了,那么发送方的goroutine就需要阻塞等待。就会把发送方的goroutine的运行情况以及现场也就是上下文,将其保存起来,生成一个sudog的结构,挂在sendq队列上。

从channel里面取数据:

当接收方取数据的时候,发现buf是空的,没有数据可以取出来,那么就会把接收方的goroutine和其上下文保存起来,生成一个sudog结构,挂在recvq队列上。

buf里面有数据的情况下取数据:

我们看到recvx取数据的索引位置是0

那么我们就从第0个位置取数据

取出数据之后,qcount即buf的数量-1,recvx即接收方索引位置+1

在buf是满的,且sendq队列中有阻塞的sudog的情况下取数据:

而后,buf里面已经空出了位置,qcount=2小于dataqsize,这时候我们要把sendq里面阻塞的sudog拿出来,开始往buf里面写数据,我们看到sendx是0,那么我们就要从第0个位置开始插入数据。

将sendq中的6放入buf的0位置

将sendq中的6放入buf的0位置后,buf的数量变成了3个,qcount+1变成了3,而sendx也从0+1变成了1

总结:

channel send:

  • chan is nil
    • block forever 永远地阻塞住了,panic
  • chan is not nil
    • 非阻塞不能发送channel的情况
      • return
    • 可以发送channel的情况
      • 全局加锁
        • recvq没有阻塞的有等待取值的队列
          • buf是满的
          • buf未满
            • 将send的数据按照sendx索引的位置放入buf
            • 更新相关索引位置
            • return
          • buf已满
            • 打包当前的send的goroutine成一个sudog
            • 将sudog放入sendq队列里阻塞起来
            • gopark() 开始进行新的一轮调度
        • recvq有阻塞的有等待取值的队列
          • 把send的数据直接拷贝到目标栈上
          • 唤醒之前被阻塞的recv goroutine
          • return

channel recv:

  • chan is nil
    • block forever 永远的阻塞起来
  • chan is not nil
    • 非阻塞且不可recv数据的情况下
      • return
    • 可以recv数据的情况下
      • 全局加锁
        • sendq队列有数据
          • dataqsize是0,也就是说chan没有缓冲区
            • 直接从sendq队列里面取出数据
            • 交给recv返回
          • dataqsize大于0
            • 根据recvx索引位置从buf上取出数据来返回
            • 如果sendq队列里面有数据,则goready()唤醒队列里第一个sudog
        • sendq队列没有数据
          • buf里面是空的
            • 打包当前recv的goroutine成一个sudog,挂在recvq队列上阻塞起来
          • buf里面有数据
            • 根据recvx索引位置从buf中取出数据
            • 更新recvx索引的位置
            • 返回

挂起与唤醒

gopark() 挂起 goready()唤醒

  • Sender挂起,一定是由receiver(或close)唤醒
  • Receiver挂起,一定是由sennder(或close)唤醒

可接管的阻塞,均是由gopark() 挂起,每一个gopark() 都会对应一个唤醒方。

动画演示channel的发送和接收流程


标题:golang 内置的 channel 数据结构是什么鬼样子呢?
作者:wangmengke
地址:http://solo.fancydigital.com.cn/articles/2021/06/02/1622644506737.html