这一期我们简单讲一讲 Go 中 channel
的实现机制。
Go 中 channel
数据结构在 src/runtime/chan.go 中定义:
查看源码并从 channel
的数据结构可以看出,它是由环形队列、类型信息、等待队列组成。
环形队列
首先, channel
里实现了一个环形队列作为其缓冲区,其长度由创建时指定,例如下面创建了一个可缓存 5 个元素的 channel:
-
dataqsiz
:指示了队列长度为 5 ,即可缓存 5 个元素; -
buf
:指向队列的内存; -
qcount
:表示队列中还有 3 个元素; -
sendx
:指示后续写入的数据存储的位置,取值[0, 5); -
recvx
:指示从该位置读取数据,取值[0, 5);
等待队列
其次,在 channel
中还有一个等待队列。
从 channel 读数据,如果 channel 缓冲区为空或者没有缓冲区,当前 goroutine 会被阻塞。向 channel 写数据,如果 channel 缓冲区已满或者没有缓冲区,当前 goroutine 也会被阻塞。
被阻塞的 goroutine 将会挂在 channel 的等待队列中:因读阻塞的 goroutine 会被向 channel 写入数据的 goroutine 唤醒;因写阻塞的 goroutine 会被从 channel 读数据的 goroutine 唤醒。
下面是一个没有缓冲区的 channel ,有几个 goroutine 被阻塞等待读数据:
一般情况下 recvq
和 sendq
至少有一个为空。但是同一个 goroutine 使用 select
语句向 channel 一边写数据,一边读数据时,两个都不为空。
一个 channel 只能传递一种类型的值,类型信息存储在 hchan 数据结构中。elemtype
代表类型,用于数据传递过程中的赋值;elemsize
代表类型大小,用于在 buf
中定位元素位置。
创建 channel
创建 channel 的过程实际上是初始化 hchan
结构。其中类型信息和缓冲区长度由 make
语句传入, buf
的大小则与元素大小和缓冲区长度共同决定。创建 channel 的源代码如下所示:
向 channel 写数据
向一个 channel 中写数据简单过程如下:
- 如果等待接收队列
recvq
不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 recvq
取出 goroutine ,并把数据写入,最后把该 goroutine 唤醒,结束发送过程; - 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入 goroutine ,将当前 goroutine 加入
sendq
,进入睡眠,等待被读 goroutine 唤醒。
简单流程图如下:
从 channel 读数据
从一个 channel 读数据简单过程如下:
- 如果等待发送队列
sendq
不为空,且没有缓冲区,直接从 sendq
中取出 goroutine ,把 goroutine 中数据读出,最后把 goroutine 唤醒,结束读取过程; - 如果等待发送队列
sendq
不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把 goroutine 中数据写入缓冲区尾部,把 goroutine 唤醒,结束读取过程; - 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
- 将当前 goroutine 加入
recvq
,进入睡眠,等待被写 goroutine 唤醒。
简单流程图如下:
关闭 channel
关闭 channel 时会把 recvq
中的 goroutine 全部唤醒,本该写入 goroutine 的数据位置为 nil
。把 sendq
中的 goroutine 全部唤醒,但这些 goroutine 会触发 panic。
除此之外,panic 出现的常见场景还有:
- 关闭值为 nil 的 channel;
- 关闭已经被关闭的 channel;
- 向已经关闭的 channel 写数据。