dropped SelectedOperation without completing the operation
主线位置:「多选一」的最后一步,
Select把预订好的操作交给调用方去完成。机制在 select。
flowchart TB s["Select::select()"] --> pre["已在选中的 channel 上预订(start_recv / start_send)"] pre --> op["返回 SelectedOperation"] op -->|"oper.recv / oper.send"| done["完成操作,mem::forget(self)"] op -->|"直接 drop"| pan["Drop:panic"] op -->|"mem::forget"| hang["留下已预订未写入的槽"]
现象
用运行时构造的 Select,拿到选中的分支后只看了 index(),忘了调 recv:
let (s, r) = unbounded::<i32>();
s.send(1).unwrap();
let mut sel = Select::new();
let i = sel.recv(&r);
let oper = sel.select();
println!("selected index {} (recv index {})", oper.index(), i);
drop(oper); // 应该是 oper.recv(&r)
实测(exp-crossbeam/pitfalls/src/bin/selected_dropped.rs,crossbeam-channel 0.5.17,退出码 101,本机路径已去掉):
selected index 0 (recv index 0)
thread 'main' (4035757) panicked at .../crossbeam-channel-0.5.17/src/select.rs:1297:9:
dropped `SelectedOperation` without completing the operation
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
带参数 complete 运行同一个程序(调了 oper.recv(&r))则正常:recv -> Ok(1)、still running; len = 0。
常见的变体:match oper.index() 里某个分支只是 continue 或 return;在分支里先做了一件可能 ? 提前返回的事,再调 oper.recv;或者把 oper 存起来、晚些再处理,中途 drop 了。
从哪来
Select::select() 返回之前已经在选中的 channel 上做了「预订」(flavor 的 start_recv / start_send,结果记在 Token 里);剩下的「把消息读出来 / 写进去」由调用方通过 SelectedOperation::send / recv 完成(预订和完成为什么要分两步,见 API 与 flavor)。这两个方法完成后 mem::forget(self) 跳过 Drop;走到 Drop 就说明没完成:
impl Drop for SelectedOperation<'_> {
fn drop(&mut self) {
panic!("dropped `SelectedOperation` without completing the operation");
}
}
这个 panic 是为了不让「已预订、未完成」的状态悄悄留下。
另外两个相关的断言在 recv / send 里:传进去的 channel 必须是预订时的那一个(「passed a receiver that wasn't selected」),否则同样 panic。
mem::forget 不是出路
mem::forget 是安全函数,可以绕过这个 panic,但绕不过问题本身:会留下一个已预订、未写入的槽。0.5.17 的 #1295 修的就是由此引起的一个 UB:list(无界)channel 的 Drop 以前会对这样的槽调析构,读的是一块未初始化的内存;修复后 Drop 只析构带 WRITE 位的槽(list)。所以在 0.5.17 之前的版本上,forget 一个预订了无界 channel 发送操作的 SelectedOperation,是安全代码能触发的 UB。
不管哪个版本,都不要用 forget 处理 SelectedOperation:把操作完成,或者改用不预订的接口。
怎么改
- 每个
index()分支都完成操作:先let msg = oper.recv(&r);,再做可能提前返回的事。select!宏会替你做这一步:它展开后在每个分支体之前先调recv/send(select),所以用宏不会踩到这条。 - 只想知道「谁就绪了」:用
Select::ready()/try_ready()/ready_timeout()。它们不预订任何东西,只返回一个就绪分支的下标,之后自己try_recv,要处理被别的线程抢先的情况(select)。 - 分支数量编译期已知:直接用
select!。
在本站
本站锁的 crossbeam-channel 是 0.5.15,在 #1295 之前。在本站仓库里搜 Select、select!、crossbeam,本站自己的代码一处都没有;本站依赖里用到 crossbeam-channel 的 tantivy、tao、wry、muda、tray-icon、global-hotkey 也都没有用 Select 或 select!(10 在本站中)。所以这条在本站没有触发路径。
小结
SelectedOperation代表一个已经预订、还没完成的操作;不调recv/send就 drop 会 panic,原文是「droppedSelectedOperationwithout completing the operation」。mem::forget能绕过 panic,但会留下一个已预订未写入的槽;0.5.17 修了 list 的Drop因此析构未初始化内存的 UB。- 用
select!宏,或只要就绪信息时用Select::ready()。 - 本站代码和依赖都不用
Select。