select! 没有按书写顺序选
主线位置:一条消息的「多选一」版本,
run_select的第一步洗牌。机制在 select。
flowchart TB sel["select! / Select::new"] --> sh["线程本地 xorshift 洗牌(固定种子)"] sh --> tr["按洗牌后的顺序 try_select"] bsel["select_biased! / Select::new_biased"] --> ord["按书写顺序 try_select"] tr --> fair["多个就绪时各分支机会均等"] ord --> prio["前面的分支一直就绪,后面的被饿死"]
现象
把控制消息的分支写在前面,以为它就绪时会先被处理:
loop {
select! {
recv(ctrl) -> msg => handle_ctrl(msg),
recv(data) -> msg => handle_data(msg),
}
}
两条 channel 都有消息时,ctrl 并不优先,大约一半的时候先处理的是 data。tokio 的 select! 默认同样随机、要写 biased; 才按顺序,两边的直觉是一致的;容易踩的是以为「写在前面就先查」。
实测
实验 E7(exp-crossbeam/select-fairness/,release):a、b 预先各放 10 万条,始终就绪;c 只在每 10 次里就绪 1 次。选 10 万次:
select! (a, b, c) a= 48554 b= 48129 c= 3317
select_biased! (a, b, c) a=100000 b= 0 c= 0
select_biased! (c, a, b) a= 90000 b= 0 c= 10000
Select::new (a, b, c) a= 48312 b= 48239 c= 3449
Select::new_biased (a, b, c) a=100000 b= 0 c= 0
两分支的版本:select! 50228 / 49772,select_biased! 100000 / 0。连续跑两次,输出逐位相同。
从哪来
run_select 非偏置时先把所有分支洗一遍再逐个 try_select,谁排第一谁赢:
if !is_biased {
// Shuffle the operations for fairness.
utils::shuffle(handles);
}
select! 和 select_biased! 展开成同一个内部宏,区别只是 const _IS_BIASED: bool 是 false 还是 true(select)。shuffle 的随机数来自线程本地 xorshift,种子固定为 1_406_868_647:同一个线程、同样的调用序列,每次运行洗出来的顺序都一样。所以「随机」在测试里不会表现为不稳定,测试写成依赖某个顺序也能一直通过,换个调用序列才露馅。
例外:只有一个 recv 分支的 select! 展开成 r.recv(),不经过 run_select,也就谈不上顺序。
怎么改
- 要优先级:
select_biased!(0.5.13,#1040)或Select::new_biased()(0.5.14,#1150),按书写顺序尝试。代价是写在前面的分支只要一直就绪,后面的分支一次也轮不到(上面select_biased! (a, b, c)那一行,b和c都是 0)。优先级分支应当是「偶尔才有」的那种,比如控制消息、关闭信号。 - 要优先级、又不想饿死:每次
select!之前先try_recv高优先级的 channel,有就处理,没有再select!。高优先级分支一直有消息时同样会饿死低优先级,这是优先级本身的含义。 - 要公平:用默认的
select!就是公平的;不要依赖分支顺序。 - 本站锁的 crossbeam-channel 0.5.15 已经有
select_biased!和Select::new_biased(两者所在的 0.5.13、0.5.14 被 yank,0.5.15 重新带出来,见 list)。本站代码和它的依赖都没有用select!/Select。
小结
select!/Select::new多分支同时就绪时随机选,靠一次 Fisher–Yates 洗牌;种子固定,所以运行间可复现。- 优先级用
select_biased!/Select::new_biased,前面的分支一直就绪时后面的会被饿死。 - E7:随机版三分支分布与期望一致,偏置版严格按书写顺序。
💬 关联讨论
发起讨论加载中...