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为空,环形队列有元素,直接从缓冲区读取数据,结束读取过程;