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 在写入时都是开启的状态

浙公网安备 33010602011771号