go chan管道读写解读

1、channel数据结构:

channel 是一种类型安全的(并发安全)、阻塞的、先进先出(FIFO)的数据结构,确保发送的数据按照发送的顺序接收。

通过 var 声明或者 make 函数创建的 channel 变量是一个存储在函数栈上的指针,指向堆上的 hchan 结构体。底层数据结构如下:


type hchan struct {

qcount uint // 队列中剩余的元素个数

dataqsiz uint // 环形队列长度,即可以存放的元素个数, make初始化时指定

// channel 分为无缓冲和有缓冲两种。

// 对于有缓冲的 channel 存储数据,使用了 ring buffer(环形缓冲区)来缓存写入的数据,本质是循环数组。

// 为什么是循环数组?普通数组不行吗?普通数组容量固定,更适合指定的空间,弹出元素时,普通数组需要全部都前移。

// 指向底层循环数组的指针(环形缓冲区)缓存区,实际上就是环形队列(有环形队列就有缓冲区,否则没有缓冲区),指向环形队列首部的指针,基于环形队列实现,大小等于make初始化channel时指定的环形队列长度,如果make初始化channel时不指定dataqsiz,则buf=0。只有缓冲型的channel才有buf

buf unsafe.Pointer

elemsize uint16 // 元素的大小

closed uint32 // channel是否关闭的标志

elemtype *_type // channel中的元素类型

// 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置

sendx uint // 写入数据的索引,即从哪个位置开始写入数据,取值[0, dataqsiz)

recvx uint // 读取数据的索引,即从哪个位置开始读取数据,取值[0, dataqsiz)

//接收等待队列,链表结构,长度无限长, 读取数据的goroutine等待队列, 如果channel的缓冲区为空或者没有缓冲区,读取数据的goroutine被阻塞,加入到recvq等待队列中。因读阻塞的goroutine会被向channel写入数据的goroutine唤醒

recvq waitq

//发送等待队列,链表结构,长度无限长, 写入数据的goroutine等待队列, 如果channel的缓冲区为满或者没有缓冲区,写入数据的goroutine被阻塞,加入到sendq等待队列中。因写阻塞的goroutine会被从channel读取数据的goroutine唤醒

sendq waitq

//并发控制锁, 同一时刻,只允许一个, channel不允许并发读写

lock mutex

}

1.1、环形队列:

chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。 下图展示了一个可缓存6个元素的channel示意图:

image.jpeg
  • dataqsiz指示了队列长度为6,即可缓存6个元素;
  • buf指向队列的内存,队列中还剩余两个元素;
  • qcount表示队列中还有两个元素;
  • sendx指示后续写入的数据存储的位置,取值[0, 6);
  • recvx指示从该位置读取数据, 取值[0, 6);

1.2、goroutine等待队列:

从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。向channel写数据,如

果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。 被阻塞的goroutine将会挂在channel的等待队列中:

  • 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
  • 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;

下图展示了一个没有缓冲区的channel,有几个goroutine阻塞等待读数据:

image.jpeg

注意,一般情况下recvq和sendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向 channel一边写数据,一边读数据。

1.3、类型信息:

一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中。

  • elemtype代表类型,用于数据传递过程中的赋值;
  • elemsize代表类型大小,用于在buf中定位元素位置;

2、channel读写流程:

G表示一个goroutine

2.1、写入:

image.jpeg

过程描述:

  • 如果等待接收队列recvq 不为空,说明没有缓冲区或者缓冲区没有数据,直接从recvq取出一个G数据写入,把G唤醒,结束发送过程;
  • 如果等待接收队列recvq为空,且缓冲区有空位,那么就直接将数据写入缓冲区sendx位置, sendx++, qcount++, 结束发送过程;
  • 如果等待接收队列recvq为空,缓冲区没有空位,将数据写入G,然后把G放到等待发送队列sendq中进行阻塞,等待被唤醒, 结束发送过程。当被唤醒的时候,需要写入的数据已经被读取出来,且已经完成了写入操作;

2.2、读取:

image.jpeg

虚线表示recvq中堵塞的G被唤醒的流程,如果G没有被唤醒,则一直堵塞下去,此时关闭channel,会得到channel类型的零值

过程描述:

  • 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,读取数据,最后把G唤醒,结束读取过程;
  • 如果等待发送队列sendq不为空,有缓冲区(此时缓冲区满了),从缓冲区中首部读出数据,把G中的数据写入缓冲区尾部,把G唤醒,结束读取过程;
  • 如果等待发送队列sendq为空,且环形队列无元素,将goruntime加入等待接收队列recvq中进行堵塞,等待被唤醒;
  • 如果等待发送队列sendq为空,环形队列有元素,直接从缓冲区读取数据,结束读取过程;
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 本篇文章内容基于go1.14.2分析 golang的chan是一个内置类型,作为csp编程的核心数据结构,其底层数...
    litesky阅读 312评论 0 0
  • GO 中 Chan 实现原理分享 嗨,我是小魔童哪吒,还记得咱们之前分享过GO 通道 和sync包的使用吗?咱们来...
    阿兵云原生阅读 415评论 0 5
  • Go语言的并发模型基于CSP(Communicating Sequential Processes)理论。Go的并...
    王侦阅读 243评论 0 0
  • G (goroutine) G是goroutine的头文字, goroutine可以解释为受管理的轻量线程, go...
    雪上霜阅读 1,239评论 0 1
  • File: chan.go chan.go这个文件是Go语言标准库中的一个重要文件,它实现了Go语言中的通道(ch...
    cuishuang阅读 216评论 0 0