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包下,其结构如下图所示:
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 加入读等待队列,并挂起等待唤醒。