select 的实现原理
kevinyan815 opened this issue · 0 comments
kevinyan815 commented
文章所有内容摘录自:Go 语言 select 实现原理
select 结构的执行过程与实现原理,首先在编译期间,Go 语言会对 select 语句进行优化,它会根据 select 中 case 的不同选择不同的优化路径:
-
空的 select 语句会被转换成调用 runtime.block 直接挂起当前 goroutine;
- runtime.block 的实现非常简单,它会调用 runtime.gopark 让出当前 goroutine 对处理器(P)的使用权并传入等待原因 waitReasonSelectNoCases。
- 简单总结一下,空的 select 语句会直接阻塞当前 goroutine,导致 goroutine 进入无法被唤醒的永久休眠状态。
-
如果 select 语句中只包含一个 case,编译器会将其转换成 if ch == nil { block }; n; 表达式;
- 首先判断操作的 Channel 是不是空的,是的话直接阻塞 goroutine;
- 然后执行 case 结构中的内容;
-
如果 select 语句中只包含两个 case 并且其中一个是 default,那么会使用 runtime.selectnbrecv 和 runtime.selectnbsend 非阻塞地执行收发操作;
-
在默认情况下会通过 runtime.selectgo 获取执行 case 的索引,并通过多个 if 语句执行对应 case 中的代码:
chosen, revcOK := selectgo(selv, order, 3) if chosen == 0 { ... break } if chosen == 1 { ... break } if chosen == 2 { ... break }
在编译器已经对 select 语句进行优化之后,Go 语言会在运行时执行编译期间展开的 runtime.selectgo 函数,该函数会按照以下的流程执行:
- 随机生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成锁定顺序 lockOrder;
- 根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel;
- 如果存在,直接获取 case 对应的索引并返回;
- 如果不存在,创建 runtime.sudog 结构体,将当前 goroutine 加入到所有相关 Channel 的收发队列,并调用 runtime.gopark 挂起当前 goroutine 等待调度器的唤醒;
- 当调度器唤醒当前 goroutine 时,会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudog 对应的索引;
select 关键字是 Go 语言特有的控制结构,它的实现原理比较复杂,需要编译器和运行时函数的通力合作。