bigywater.tech
Go · 83

select 与超时:time.After 在循环里的坑

select 的几条规则决定了很多并发代码的行为:多个就绪时随机选、已关闭的 channel 一直就绪、nil channel 永远阻塞。这篇逐条用实验验证,并给出实现优先级和动态禁用 case 的写法。

50.0% / 50.0%
两个 channel 总是同时就绪时,select 选中各自的比例
0 次
嵌套 select 让高优先级先走时,低优先级被选中的次数(普通 select 约一半)
12 次
已关闭的 channel 置为 nil 后,合并循环的迭代次数(不置 nil 空转 300 万次)

就绪时是随机选

实测 · 本机执行输出
  两个 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。

关闭的 channel 与 nil

实测 · 本机执行输出
  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 永远阻塞。

分步演示

select 的行为
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。每个实验是单独的小程序;公平性、优先级各 100 万次,撞车 10 万次;CPU 时间由 syscall.Getrusage 读自己;不处理已关闭 channel 的那次在 300 万次迭代时被代码中断;规范引用来自本机 $GOROOT/doc/go_spec.html,确认原文存在
    • 没有验证:select 随机选择的具体算法(没有读运行时源码)、case 很多时的开销、select 与 context 超时的组合代价(参见 time.After 的另一篇)、发送 case 的随机性、不同 GOMAXPROCS 下的差别
    京ICP备2026067502号-1京公网安备11010502063487号