编译器会使用如下的流程处理 select 语句:
- 将所有的 case 转换成包含 channel 以及类型等信息的 runtime.scase 结构体。
- 调用运行时函数 runtime.selectgo 从多个准备就绪的 channel 中选择一个可执行的 runtime.scase 结构体。
- 通过 for 循环生成一组 if 语句,在语句中判断自己是不是被选中的 case。
一个包含三个 case 的正常 select 语句会被展开成以下逻辑:
selv := [3]scase{}
order := [6]uint16
for i, cas := range cases {
c := scase{}
c.kind = ...
c.elem = ...
c.c = ...
}
chosen, revcOK := selectgo(selv, order, 3)
if chosen == 0 {
...break
}
if chosen == 1 {
...break
}
if chosen == 2 {
...break
}
最重要的是 selectgo 这个函数:
- 执行必要的初始化操作并决定处理 case 的两个顺序:轮询顺序 pollOrder 和加锁顺序 lockOrder。
- 轮询顺序:通过 runtime.fastrandn 函数引入随机性。
- 加锁顺序:按照 Channel 的地址排序后确定加锁顺序。
随机的轮询顺序可以避免 channel 的饥饿问题,保证公平性。而根据 channel 的地址顺序确定加锁顺序能够避免死锁的发生。
- 按照加锁顺序 lockOrder锁住所有 channel。
- 遍历所有的 channel,查看其是否可读或者可写。如果其中的 channel 可读或者可写,则解锁所有 channel,并返回对应的 channel 数据。
- 假如没有 channel 可读或者可写,但是有 default 语句,则返回 default 语句对应的 scase 并解锁所有的channel。
- 假如既没有 channel 可读或者可写,也没有 default 语句,则将当前运行的 groutine 阻塞,并加入到当前所有channel的等待队列中,然后解锁所有 channel,等待被唤醒。
- select 中一些 channel 可读或者可写了,当前 goroutine 被唤醒,并再次加锁所有channel。遍历所有channel找到那个对应的 channel 和 sudog,唤醒 sudog,并将没有成功的 sudog 从所有 channel 的等待队列中移除。
- 如果对应的scase值不为空,则返回需要的值,并解锁所有channel。如果对应的scase为空,则循环此过程。
select 和 channel 的关系: