select 的几条规则决定了很多并发代码的行为:多个就绪时随机选、已关闭的 channel 一直就绪、nil channel 永远阻塞。这篇逐条用实验验证,并给出实现优先级和动态禁用 case 的写法。
两个 channel 总是同时就绪,select 100 万次:选中第一个 499725 次,第二个 500275 次(50.0% / 50.0%)
两个 channel 同时就绪,100 万次 select 里各被选中约一半。写在前面的 case 没有优先权,规范说是均匀伪随机选一个。
高、低优先级 channel 总是同时就绪,各 100 万次:普通 select 选中低优先级 499630 次;先单独试高优先级再兜底的嵌套 select 选中低优先级 0 次
想让高优先级先走,先单独做一次带 default 的 select 试高优先级,没有再进入同时等两个的 select。实验里这个写法让低优先级一次都没被选中;普通 select 则选中了约一半。副作用是低优先级可能饿死。
结果和超时同时就绪(定时器已到期、结果也已放进 channel),10 万次:选中“结果” 50017 次,选中“超时” 49983 次
结果和超时同时就绪时,谁被选中是抛硬币(10 万次里 50017 对 49983)。如果业务上“结果到了就算成功”,要在超时分支里再检查一次结果 channel。
a 很快关闭,b 要 200 ms 后才有数据。关闭后不处理(继续 select 已关闭的 a):循环了 3000001 次,进程 CPU 时间 0.115 秒 a 很快关闭,b 要 200 ms 后才有数据。关闭后把 a 置为 nil:循环了 12 次,进程 CPU 时间 0.000 秒
已关闭的 channel 一直就绪(读到零值和 ok=false)。合并循环里不处理它,会一直空转(我在 300 万次时中断,CPU 0.115 秒);把变量置为 nil 后这个 case 永远不就绪,循环只走了 12 次。
select + default 的忙等待 300 ms:循环 8167784 次,CPU 时间 0.300 秒 阻塞的 <-ch 等 300 ms:CPU 时间 0.000 秒
select 加 default 的循环 300 ms 占满一个核(CPU 0.300 秒),而阻塞等待是 0。只有真的需要“不阻塞地试一下”才用 default。
If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. Since communication on nil channels can never proceed, a select with only nil channels and no default case blocks forever.
两条规则都在语言规范里:多个就绪均匀随机选一个;只含 nil channel 又没有 default 的 select 永远阻塞。