Go 语言入门指南——Channel底层原理 | 青训营

98 阅读4分钟

1. 概念

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

2. 应用场景

  • 数据交流:当作并发的 buffer 或者 queue,解决生产者 - 消费者问题(生产方和消费方解耦)。多个 goroutine 可以并发当作生产者(Producer)和消费者(Consumer)。

  • 数据传递:一个goroutine将数据交给另一个goroutine,相当于把数据的拥有权托付出去。

  • 信号通知:一个goroutine可以将信号(closing,closed,data ready等)传递给另一个或者另一组goroutine。

  • 任务编排:可以让一组goroutine按照一定的顺序并发或者串行的执行,这就是编排功能。

  • 锁机制:利用channel实现互斥机制。

  • 定时任务。

3. 底层数据结构

通过var声明或者make创建的channel变量是一个存储在函数栈帧上的指针,占用8个字节,指向堆上的hchan结构体

即:channel的底层是hchan的结构体,在Go的runtime包下,其结构如下图所示:

未命名图片.png

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不允许并发读写
}

说明:

(1)Buf: 指向底层循环数组的指针(环形缓冲区)。如果这个channel是有缓冲的,那么它就会用到这个缓冲数组

(2)Senddq:表示待发送数据队列;读等待队列 ---> 存放尝试读取channel的数据但是被阻塞的协程

(3)recvq:表示待接收数据队列;写等待队列 ---> 存放尝试读取channel的数据但是被阻塞的协程

注意注意:上面存放的是协程啊!!!

(4)lock:互斥锁。因为channel是线程安全,所以有一把锁,保证我们的操作都是互斥的。

4. 创建channel

创建时首先会做一些检查: (1)判断元素大小不能超过64K (2)判断元素的对齐大小不能超过8字节 (3)判断计算出来的内存是否超过限制

然后:

如果是无缓冲的chanhel,会直接给hchan分配内存;

如果是有缓冲的channel,并且元素不包含指针,那么会为 hchan和底层数组分配一段连续的地址;

如果是有缓冲的channel,并且元素包含指针,那么会为hchan和底层数组分别分配地址。

5. 向channel中发送数据原理

向channel中发送数据时大概分为两大块:检查和数据发送,数据发送流程如下:

(1)如果channel的读等待队列存在接收者goroutine,那么

将数据直接发送给第一个等待的goroutine,唤醒接收的goroutine。

(2)如果channel的读等待队列不存在接收者goroutine,那么

  • 如果循环数组buf未满,那么将会把数据发送到循环数组buf的队尾;

  • 如果循环数组buf已满,这个时候就会走阻塞发送的流程,将当前goroutine加入写等待队列,并挂起等待唤醒。

6. 从channel中接收数据原理

从channel 中接收数据时大概分为两大块,检查和数据发送,而数据接收流程如下:

(1)如果channel的写等待队列存在发送者goroutine,那么

  • 如果是无缓冲channel,直接从第一个发送者goroutine那里把数据拷贝给接收变量,唤醒发送的goroutine;

  • 如果是有缓冲channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutine的数据拷贝到buf循环数组队尾,唤醒发送的goroutine。

(2)如果channel的写等待队列不存在发送者goroutine,那么

  • 如果循环数组buf非空,将循环数组buf的队首元素拷贝给接收变量;

  • 如果循环数组buf为空,这个时候就会走阻塞接收的流程,将当前goroutine 加入读等待队列,并挂起等待唤醒。