Golang Channel 实现原理
前言#
最近真高产啊, 果然有压力就有动力了, 不知道能保持多久, 真是痛并快乐着
什么是 Channel#
channel
常用来作为多个协程直接通信, 在真正编写代码中使用频率非常高, 他就是一个管道, 先进先出, 那么, 作为程序员, 有必要了解channel
的实现原理
数据结构#
那么, 和往常一样, 还是从数据结构开始看起
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁,chan不允许并发读写
}
环形队列#
chan 内部实现了一个环形队列作为缓冲区, 你指定的缓冲区的长度就是这个环形队列的长度
比如make(chan int, 6)
, 在多次进行操作之后, 他的数据可能是
{
qcount: 2, // 代表队列中还有2个元素
dataqsiz: 6, // 代表队列长度(缓冲区)为6
buf: [0, 1, 1, 0, 0, 0], // 真正的数据, 假设两个数据都是1
sendx: 3, // 后续需要增加数据时, 从 buf 索引3开始
recvx: 1, // 需要读取数据是, 从 buf 索引1开始
}
等待队列#
我们知道, 如果缓冲区已满或者没有消费者获取数据时, 向 channel 写入数据会阻塞
我们也知道, 如果缓冲区已空或者没有生产者生产数据时, 从 channel 获取数据也会阻塞
对于一个 channel 来讲, 其生产者和消费者的 goroutine 存储在 hchan 的recvq
和sendq
中
recvq
: 等待从 channel 读取数据的 goroutinesendq
: 等待向 channel 写入数据的 goroutine
他们是互相唤醒的, 比如
- 当 goroutine 向 channel 写入数据时, 会唤醒
recvq
里的 goroutine - 当 goroutine 从 channel 读取数据是, 会唤醒
sendq
里的 goroutine
一般情况下 recvq
和sendq
至少有一个为空, 也就是说一般情况下不可能存在既有向 channel 写入数据的 goroutine 等待, 又有从 channel 获取数据的 goroutine 等待. 除非在同一个 goroutine 中使用 select
向 channel 一边写一边读
类型信息#
在 channel 中, make 的时候就必须设定这个 channel 中存储的值的类型, 对应的字段如下
elemtype
每个元素的类型elemsize
类型的大小, 主要用来在 buf 里定位元素
锁#
channel 内部实际上有一个互斥锁来保证同时仅允许被一个 goroutine 进行操作, 关于互斥锁原理可以查看我的博客, 这里不多赘述
锁在hchan
的字段是 lock
创建 channel#
创建 channel 使用 make, 例如make(chan int, 6)
, 在 make 时, 就确定了 hchan 的buf/elemsize/elemtype/dadaqsize
字段
写数据#
向 channel 中写入数据的简单过程如下:
- 判断
recvq
是否为空, 不为空则代表有 goroutine 在等待读数据, 也就是说缓冲区中没有数据了或者没有缓冲区, 此时会直接从recvq
中取出 goroutine, 直接把 goroutine 唤醒并且将数据写入, 结束写入过程 - 如果
recvq
为空, 代表此时没有 goroutine 在读数据, 此时, 如果 channel 中有缓冲区且缓冲区中有空余位置, 则直接将数据写入缓冲区, 结束过程 - 如果 channel 缓冲区已满或者没有缓冲区时, 将等待发送的数据写入当前 goroutine 中, 同时将当前 goroutine 加入到
sendq
, 等待被读取的goroutine
唤醒
读数据#
读数据与写数据类似, 过程如下:
- 判断
sendq
是否为空, 不为空则代表有 goroutine 在等待写数据, 此时判断缓冲区是否有值, 如果缓冲区有数据, 则从缓冲区首部(recvx
)读取一个数据, 同时唤醒sendq
中的等待写入 goroutine, 通知其写入数据, 结束流程 - 如果没有缓冲区, 则从
senq
中获取等待发送数据的 goroutine, 获取数据并唤醒这个 goroutine, 结束流程 - 如果当前
sendq
为空且缓冲区为空, 代表此时没有数据, 这时将 goroutine 加入到recvq
中, 等待被写 goroutine 唤醒
关闭#
关闭 channel 会把recvq
中的 goroutine 全部唤醒, 然后将里面存储的数据设置为 nil. 然后将senq
中的 goroutine 全部唤醒, 但是这些 goroutine 会导致 panic, 所以必须要确保这个 channel 在写入时都是开启的状态
作者:chnmig
出处:https://www.cnblogs.com/chnmig/p/16744175.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix