channel 的底层原理

Posted hongmingover

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了channel 的底层原理相关的知识,希望对你有一定的参考价值。

三、channel 的底层原理

前面提及过 channel 创建后返回了 hchan 结构体,现在我们来研究下这个结构体,它的主要字段如下:

type hchan struct 
 qcount   uint   // channel 里的元素计数
 dataqsiz uint   // 可以缓冲的数量,如 ch := make(chan int, 10)。 此处的 10 即 dataqsiz
 elemsize uint16 // 要发送或接收的数据类型大小
 buf      unsafe.Pointer // 当 channel 设置了缓冲数量时,该 buf 指向一个存储缓冲数据的区域,该区域是一个循环队列的数据结构
 closed   uint32 // 关闭状态
 sendx    uint  // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已发送数据的索引位置
 recvx    uint  // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已接收数据的索引位置
 recvq    waitq // 想读取数据但又被阻塞住的 goroutine 队列
 sendq    waitq // 想发送数据但又被阻塞住的 goroutine 队列

 lock mutex
 ...


channel 在进行读写数据时,会根据无缓冲、有缓冲设置进行对应的阻塞唤起动作,它们之间还是有区别的。下面我们来捋一下这些不同之处。

无缓冲 channel

由于对 channel 的读写先后顺序不同,处理也会有所不同,所以,还得再进一步区分:

channel 先写再读

在这里,我们暂时认为有 2 个 goroutine 在使用 channel 通信,按先写再读的顺序,则具体流程如下:

channel 用法和底层原理

可以看到,由于 channel 是无缓冲的,所以 G1 暂时被挂在 sendq 队列里,然后 G1 调用了 gopark 休眠了起来。

接着,又有 goroutine 来 channel 读取数据了:

channel 用法和底层原理

此时 G2 发现 sendq 等待队列里有 goroutine 存在,于是直接从 G1 copy 数据过来,并且会对 G1 设置 goready 函数,这样下次调度发生时, G1 就可以继续运行,并且会从等待队列里移除掉。

channel 先读再写

先读再写的流程跟上面一样。

channel 用法和底层原理

G1 暂时被挂在了 recvq 队列,然后休眠起来。

G2 在写数据时,发现 recvq 队列有 goroutine 存在,于是直接将数据发送给 G1。同时设置 G1 goready 函数,等待下次调度运行。

channel 用法和底层原理

有缓冲 channel

在分析完了无缓冲 channel 的读写后,我们继续看看有缓冲 channel 的读写。同样的,我们分为 2 种情况:

channel 先写再读

这一次会优先判断缓冲数据区域是否已满,如果未满,则将数据保存在缓冲数据区域,即环形队列里。如果已满,则和之前的流程是一样的。

当 G2 要读取数据时,会优先从缓冲数据区域去读取,并且在读取完后,会检查 sendq 队列,如果 goroutine 有等待队列,则会将它上面的 data 补充到缓冲数据区域,并且也对其设置 goready 函数。

channel 用法和底层原理

channel 先读再写

此种情况和无缓冲的先读再写是一样流程,此处不再重复说明。

四、总结

有缓冲 channel 和无缓冲 channel 的读写基本相差不大,只是多了缓冲数据区域的判断而已。

channel 在使用的时候大多时候得和 select 配合使用,尽管只需要简单的用 <- ch 和 ch <- 来读写数据,但它的底层还是很有讲究的,特别是涉及到调度的休眠唤起。

这也能看出 Go 的精妙之处:复杂底层,优雅运用。

以上是关于channel 的底层原理的主要内容,如果未能解决你的问题,请参考以下文章

channel 的底层原理

channel 的底层原理

图解Go的channel底层原理

channel底层原理

从鹅厂实例出发!分析Go Channel底层原理

最全Go select底层原理,一文学透高频用法