Go 语言 chan 实现原理,彻底搞懂 chan 读写机制
在之前的文章中,我们有介绍过 channel 的使用,传送门。比较经典的一句话就是:
在 Go 语言中,提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是提倡通过 channel 发送接收消息的方式进行数据传递。
这里我们再更加深层次的了解下 chan 。
chan 数据结构
src/runtime/chan.go
中定义了 channel 的数据结构如下:
环形队列
chan 内部实现了一个环形队列来作为缓冲区,队列的长度是在创建 chan 的时候所指定的。
如下图,我们创建一个可缓存 6 个元素的 channel 示意图:
dataqsiz 指示了队列长度为 6,即可缓存 6 个元素;
buf 指向队列的内存,队列中还剩余两个元素;
qcount 表示队列中还有两个元素;
sendx 指示后续写入的数据存储的位置,取值范围为
[0,6)
;recvx 指示从该位置读取数据,取值范围为
[0,6)
;
等待队列
从 channel 中读取数据,如果 channel 的缓冲区为空,或者没有缓冲区,那么当前的 goroutine 会被阻塞。
向 channel 中写入数据,如果 channel 的缓冲区已满,或者没有缓冲区,那么当前的 goroutine 会被阻塞。
被阻塞的 goroutine 会挂在 channel 的等待队列中。
因为读所导致的阻塞,会被向 channel 写入数据的 goroutine 所唤醒。
因为写所导致的阻塞,会被从 channel 读取数据的 goroutine 所唤醒。
如下图,为没有缓冲区的 channel,recvq
中有几个 goroutine 阻塞等待读数据。
注意,一般情况下 recvq 和 sendq 至少有一个为空。只有一个例外,那就是同一个 goroutine 使用 select 语句向 channel 一边写数据,一边读数据。
类型信息
一个 channel 只能传递一种类型的值,类型信息存储在 hchan 数据结构中:
elemsize :类型大小,用于在 buf 中定位元素位置。
elemtype :类型,用于数据传递过程中的赋值;
锁
我们知道,channel 是并发安全的,即一个 channel 同时仅允许被一个 goroutine 读写。
channel 读写
创建 channel
创建 channel 其实就是初始化 hchan 结构,其类型信息和缓冲区长度由 make 语句传入,buf 的大小则与元素大小和缓冲区长度来共同决定。
创建 channel 的伪代码:
向 channel 写数据
过程如下:
若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。
若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。
从 channel 读数据
过程如下:
若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程;
如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程;
如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒;
关闭 channel
关闭 channel 时会将 recvq 中的 G 全部唤醒,,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic。
panic 出现的场景还有:
关闭值为 nil 的 channel
关闭已经关闭的 channel
向已经关闭的 channel 中写数据
用法
单项 channel
只能发送或者只能接收的 channel 为单向 channel。
单向 channel 声明
只需要在基础声明中增加操作符即可:
示例:
select
select 可以实现多路复用,即同时监听多个 channel。
发现哪个 channel 有数据产生,就执行相应的 case 分支
如果同时有多个 case 分支可以执行,则会随机选择一个
如果一个 case 分支都不可执行,则 select 会一直等待
示例:
运行结果:
select 的 case 语句读 channel 不会阻塞,尽管 channel 中没有数据。这是由于 case 语句编译后调用读 channel 时会明确传入不阻塞的参数,此时读不到数据时不会将当前 goroutine 加入到等待队列,而是直接返回。
range
通过 range 可以持续从 channel 中读取数据,类似于遍历,当 channel 中没有数据时会阻塞当前 goroutine ,与读 channel 时阻塞处理机制一样。
示例:
注意:如果向此 channel 写数据的 goroutine 退出时,系统检测到这种情况后会 panic,否则 range 将会永久阻塞。
评论