【Go笔记】通道与协程间通信
CSP并发编程
- CSP (Communicating Sequential Process,通信顺序进程) - 用于描述鬓发系统中交互模式的形式化语言,通过通道来传递信息
- 进程之间通过消息传递来进行通信,而不是通过共享内存,各进程独立运行,只有在通信点,也就是channel上进行同步和数据交换
- 通信通常是同步的,只有发送方和接收方都做好准备了才会进行数据交换,因此会堵塞
- 避免了共享内存的数据竞争和锁问题
- 天然支持高并发和分布式
- 方便设计和推理验证工程设计
通道的基本使用方式
- 使用箭头
<-
代表数据流通方向,可以流入通道也可以从通道流出 - 使用
var {name} chan T
来定义通道及其传输的数据类型 - 通道的表示形式有三种,
chan T
,chan <- T
,<- chan T
,分别代表可读写,只写和只读 - 未被初始化的通道是
nil
,使用make()
来初始化通道 - 对于无缓冲通道,能够向通道写入数据的前提是有另外一个协程在读取通道,否则当前协程会进入休眠状态,直到成功写入,即同步堵塞
- 读取也是一样,如果一个通道没有数据流出,就会将当前协程堵塞,进入休眠状态,直到通道有数据出来。有两种方式
data := <-c
和data, ok := <-c
,后者表示通道读取完毕后是否被关闭 - 关闭通道用内置的
close(c)
函数就可以了,可以向未初始化的通道写入数据,虽然无效;但是不能向已经关闭了的通道写入数据。 - 通道关闭的时候,堵塞在读取通道处的语句会自动解除堵塞并获取该通道的默认零值
- 不能重复关闭通道,也不能关闭未被初始化的通道。
- 事实上,也并不需要关心是否所有通道都被关闭了,gc会自动关闭回收未被引用的通道
- 通道是Go中的引用类型而不是值类型,因此传递到其他协程中的通道实际引用了同一个通道
- 如果使用
for n := c {...}
这样的循环来遍历通道的话,for循环会等到c关闭才会退出 - 普通通道可以隐形转换为单向通道,反之不可
select多路复用
- 当多个通道同时准备好的时候,select会随机选择一个case来执行
- 如果没有任何一个通道准备好,select就会堵塞,永远陷入等待。此时可以加上default来应对所有通道都没有准备好而堵塞的情况,也可以使用
time.After(...)
,这会定时发送一个信号,以此完成超时控制 - 可以将for与select结合来重复执行select,再使用
time.Tick(...)
来设定一个间隔任务 - 由于nil通道无论如何都会被堵塞,所以该case永远不会被执行,但最好不要把通道设置为nil,因为这是不可逆操作
通道底层原理
¶初始化
1 |
|
- buf中的数据虽然是线性的结构,但是通过数组和需要
recvx
,sendx
可以模拟出环形结构以重复利用空间 - 初始化时,分配大小为0时,只分配
hchan
结构体大小;元素不包含指针时,连续分配hchan
与size
元素大小;包含指针时,单独分配内存空间,以进行正常gc
¶写入
- 会调用
runtime/chan.go
里面的chansend
执行 - 分为三种情况:有正在等待的读取协程,缓冲区有空余,缓冲区无空余
- 有正在等待的读取协程 - 准备获取的协程会被放在
recvq
的队列里面,直接取出第一个然后将元素复制到对应的协程中,然后再唤醒 - 缓冲区有空余 - 如果没有整下等待的协程,但是有缓冲区且没有满,那就写到缓冲区
buf
里面 - 缓冲区无空余 - 如果无缓冲区或者满了,那就把发送的协程丢到
sendq
的末尾然后让它休眠,等待唤醒
¶读取
- 调用了
chanrecv
函数,与写入的原理非常类似,依然是三种情况 - 有正在等待的写入协程 - 直接从
sendq
里面获取第一个写入协程,然后将正在写入的元素直接复制到当前协程中,当前协程不需要休眠 - 缓冲区有元素 - 如果没有写入协程,但是有缓冲区且有数据,那就直接读取缓冲区的数据
- 缓冲区无数据 - 如果没有缓冲区或者里头没有数据,那就直接丢到
recvq
末尾等待被唤醒
select底层原理
- 只有一个通道的时候,编译器会直接优化成普通等待操作
- 每一个case都对应一个结构体
1 |
|
- scase有四种类型,分别为
caseNil
,caseRecv
,caseSend
和caseDefault
,分别代表分支通道为nil,分支从通道接受信息,分支从通道发送信息,default分支,每种通道会有不同的行为 - 首先通过
pollorder
和lockorder
处理序列,前者类似于洗牌算法,保证随机性;后者按照地址大小对通道地址排序,便于对通道一次加锁,避免多个协程并发加锁的时候的死锁问题 - 随后进入一轮循环,主要找出是否有准备好的分支,有就执行,没有就看有咩有default,有就执行
- 当一轮循环完成且不能退出时,意味着需要进入休眠状态并等待通道。此时进入二轮循环,读取和写入通道都要创建一个新的
sudog
并放到对应通道的等待队列,然后开始休眠。当有任意通道不堵塞了,协程将被唤醒。需要将sudog
结构体在其他通道的等待队列中出栈,因为已经被唤醒了。
【Go笔记】通道与协程间通信
https://study.0x535a.cn/go-note/go-channel/