一、定义
Channel 是 goroutine 之间的通信方式,Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通信来共享内存,而不是共享内存来通信。
gorotine由 runtime来管理
二、创建
创建有缓冲的缓存
1 | ch := make(chan int, 2) //创建channel,类型为char, buffer大小是2 |
无缓冲的channel
1 | ch := make(chan int) //创建channel |
ch是存在于函数栈帧上的指针,指向对上的hchan数据结构
底层数据结构
1 | type hchan struct { |
- lock:是互斥锁。channel需要支持goroutine间的并发访问,所以需要锁来保护整个数据结构
- buf:对于有缓冲区的channel特有的结构,用于缓存数据。是个环形链表
- qcount:缓冲区最多存储元素的个数
- elemtype:每个元素占多大空间
- elemtype *_type:golang运行时中(runtime)内存复制、垃圾回收等机制,依赖数据的类型信息,因此需要有该指针,指向元素类型的类型元数据
- sendx/recvx:接收和发送指针 channel支持交替地读和写,需要记录读(接收)、写(发送)的下标
- recvq/sendq:接收和发送的等待队列,是双向队列。当读和写不能立即完成时,需要让当前协程在channel上等待。当条件满足时,要能够立即唤醒等待的协程,因此需要两个等待队列,分别针对读(接收)和写(发送)
- closed:channel能够被关闭,要记录它的状态
三、发送&接收数据
3.1 发送&接收数据过程
1 | ch := make(chan int, 5) |
1、由于没有协程在等待接收数据,故所有数据都写入缓冲区中。sendx从下标为0开始向后移动,移动到下标4时回重新返回下标0的位置,因此channel的缓冲区是环形缓冲区。
2、此时缓冲区已经没有空闲位置,第6个数据无处可放。此时,g1会以sudog的形式,进入(存储)到发送等待队列(sendq)
中。
sudog是一个链表,里面记载着g1、ch、元素等信息。
3、协程g2从ch中接收一个元素,recvx指向下一个位置。
4、此时有空位会唤醒sendq中的g1,将(sudog记录着的)数据6发送给ch,sendx向后移动,此时缓冲区再次满了,sendq队列为空;
3.2 发送数据
发送数据的写法
1 | ch <- 10 //10发送到ch (协程向ch中发送数据) |
非阻塞情况
1、缓冲区(buffer)还有位置
2、无缓冲区 && 但有协程在接收队列(recvq)等待接收数据
阻塞的情况
1、ch == nil
2、无缓冲区 && 没有协程等着接收数据
3、有缓冲区但已经满 && 没有协程等着接收数据
1 | //避免阻塞的写法 |
3.3接收数据
接收数据的写法:
1 | <-ch //会将接收到的结果丢弃 |
非阻塞的情况
1、有缓冲区,且数据未满
2、满或无缓冲区 && 但sendq中有协程等着发送数据
阻塞的情况
1、ch == nil
2、无缓冲区 && 无协程在sendq等着发送
3、有缓冲区但没数据 && 无协程在sendq等着发送
避免发送阻塞的写法:
1 | select { |
四、应用代码
一道笔试题:
通过channel和2个goroutine交替打印出12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728
1 | package main |
五、参考
https://www.bilibili.com/video/BV1hv411x7we?p=29&vd_source=d85f289ebf4d31920c7178bbb563a0a4