select 能在 Channel 上进行非阻塞的收发操作; select 在遇到多个 Channel 同时响应时,会随机执行一种情况;
default
go channel从非阻塞收发演变到阻塞收发
数据结构
select 在 Go 语言的源代码中不存在对应的结构体,但是我们使用 runtime.scase 结构体表示 select 控制结构中的 case:
|
|
select 不存在任何的 case; select 只存在一个 case; select 存在两个 case,其中一个 case 是 default; select 存在多个 case;
空的 select 语句会直接阻塞当前 Goroutine,导致 Goroutine 进入无法被唤醒的永久休眠状态。
单一管道改写:
|
|
非阻塞操作
当 select 中仅包含两个 case,并且其中一个是 default 时,Go 语言的编译器就会认为这是一次非阻塞的收发操作。cmd/compile/internal/gc.walkselectcases 会对这种情况单独处理。不过在正式优化之前,该函数会将 case 中的所有 Channel 都转换成指向 Channel 的地址,我们会分别介绍非阻塞发送和非阻塞接收时,编译器进行的不同优化。
在默认的情况下,编译器会使用如下的流程处理 select 语句:
将所有的 case 转换成包含 Channel 以及类型等信息的 runtime.scase 结构体;
调用运行时函数 runtime.selectgo 从多个准备就绪的 Channel 中选择一个可执行的 runtime.scase 结构体;
通过 for 循环生成一组 if 语句,在语句中判断自己是不是被选中的 case;
|
|
重点: runtime.selectgo
执行一些必要的初始化操作并确定 case 的处理顺序;
在循环中根据 case 的类型做出不同的处理;
初始化:
runtime.selectgo 函数首先会进行执行必要的初始化操作并决定处理 case 的两个顺序 — 轮询顺序 pollOrder 和加锁顺序 lockOrder:
pollorder:
将cases的顺序随机打乱
lockorder: 按照chan在堆区的地址顺序进行排序
循环:
当我们为 select 语句锁定了所有 Channel 之后就会进入 runtime.selectgo 函数的主循环,它会分三个阶段查找或者等待某个 Channel 准备就绪:
查找是否已经存在准备就绪的 Channel,即可以执行收发操作;
将当前 Goroutine 加入 Channel 对应的收发队列上并等待其他 Goroutine 的唤醒;
当前 Goroutine 被唤醒之后找到满足条件的 Channel 并进行处理;
由于当前的 select 结构找到了一个 case 执行,那么剩下 case 中没有被用到的 sudog 就会被忽略并且释放掉。为了不影响 Channel 的正常使用,我们还是需要将这些废弃的 sudog 从 Channel 中出队。
pass 1 - look for something already waiting 根据确定好的pollorder顺序,遍历,找到第一个ready的channel,执行对应逻辑
我们简单总结一下 select 结构的执行过程与实现原理,首先在编译期间,Go 语言会对 select 语句进行优化,它会根据 select 中 case 的不同选择不同的优化路径:
空的 select 语句会被转换成调用 runtime.block 直接挂起当前 Goroutine;
如果 select 语句中只包含一个 case,编译器会将其转换成 if ch == nil { block }; n; 表达式;
首先判断操作的 Channel 是不是空的;
然后执行 case 结构中的内容;
如果 select 语句中只包含两个 case 并且其中一个是 default,那么会使用 runtime.selectnbrecv 和 runtime.selectnbsend 非阻塞地执行收发操作;
在默认情况下会通过 runtime.selectgo 获取执行 case 的索引,并通过多个 if 语句执行对应 case 中的代码;
在编译器已经对 select 语句进行优化之后,Go 语言会在运行时执行编译期间展开的 runtime.selectgo 函数,该函数会按照以下的流程执行:
随机生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成锁定顺序 lockOrder;
根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel;
如果存在,直接获取 case 对应的索引并返回;
如果不存在,创建 runtime.sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列,并调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒;
当调度器唤醒当前 Goroutine 时,会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudog 对应的索引;
单个channel select有可能会被阻塞