SpawnToken instances may not be dropped
主线位置:定时器主线开头,任务从「占好格子」到「进运行队列」之间。令牌是什么、为什么不能丢,见 Spawner 的「
SpawnToken:占好了格子、还没进执行器」。
stateDiagram-v2 [*] --> Free Free --> Claimed: task fn returns Ok Claimed --> Queued: spawner.spawn Queued --> Free: future Ready Claimed --> Panic: token dropped
实验 crate exp-embassy/pita-std,bin token_drop、token_bare、token_let、token_fixed(platform-std,debug);M2 Max、macOS 27、rustc 1.99.0。
现象
启动函数先拿令牌,再解析配置,配置有错就用 ? 返回:
fn start(spawner: Spawner, raw_period: &str) -> Result<(), StartError> {
let token = sensor(100).map_err(StartError::Spawn)?;
let period = parse_period(raw_period)?; // returns early: `token` is dropped here
println!("period {period}");
spawner.spawn(token);
Ok(())
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let r = start(spawner, "1OO");
println!("start: {r:?}");
std::process::exit(0);
}
配置里的 1OO(字母 O)解析失败。期望看到 start: Err(BadConfig(...)),实际是:
thread 'main' (5033649) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-executor-0.10.0/src/spawner.rs:54:9:
SpawnToken instances may not be dropped. You must pass them to Spawner::spawn()
真正的错误(配置不对)永远打印不出来,panic 的位置在 embassy-executor 里,看不出是哪个令牌。
另一条来路是忘了 spawn。只写 sensor();(bin token_bare),rustc 给一条警告:
warning: unused `Result` that must be used
--> src/bin/token_bare.rs:9:3
|
9 | sensor();
| ^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
|
9 | let _ = sensor();
| +++++++
运行时 panic,消息同上。照 help 改成 let _ = sensor();(bin token_let),警告没了,panic 还在:let _ = 立刻 drop 了整个 Result,里面的令牌跟着 drop。
出处
impl<S> Drop for SpawnToken<S> {
fn drop(&mut self) {
// TODO deallocate the task instead.
panic!("SpawnToken instances may not be dropped. You must pass them to Spawner::spawn()")
}
}
令牌存在时,池里那一格已经被占用、future 已经构造好,只是还没进任何执行器的队列。丢掉它,这一格既不会被 poll,也不会被释放(释放发生在 future 返回 Ready 时),所以库选择当场 panic,而不是悄悄漏掉一格;TODO 说明作者也认为该改成释放。Spawner::spawn 内部先 mem::forget(token) 再入队,正常路径上 Drop 不会运行。
SpawnToken 自己带着一条 #[must_use = "Calling a task function does nothing on its own. You must spawn the returned SpawnToken, typically with Spawner::spawn()"]。0.10.0 起任务函数返回的是 Result<SpawnToken<_>, SpawnError>(见 Busy - Too many instances of this task are already running),外面一层 Result 的 must_use 先生效,上面那条警告说的是「may be an Err variant」,令牌自己的那句提示看不到了,help 也就给出了一个会 panic 的建议。
修法
把所有可能失败的事放在创建令牌之前,创建之后立刻交给 spawn(bin token_fixed):
fn start(spawner: Spawner, raw_period: &str) -> Result<(), StartError> {
let period = parse_period(raw_period)?;
spawner.spawn(sensor(period).map_err(StartError::Spawn)?);
Ok(())
}
start: Err(BadConfig("period is not a number"))
start: Ok(())
sensor every 100 ms
第一次调用拿到了真正的错误,第二次正常启动。spawner.spawn(task(..)?) 写在一个表达式里,令牌没有机会活到下一条语句。
怎么确认没中招
- 任务函数的返回值只出现在两种位置:
spawner.spawn(task(..)?)/spawner.spawn(task(..).unwrap()),或者match task(..) { Ok(t) => spawner.spawn(t), Err(e) => ... }。看到let token = task(..)就检查它和spawn之间有没有?、return、break、会 panic 的调用。 - 不要用
let _ = task(..)消除警告。clippy 的let_underscore_must_use(restriction 组,默认不开)能抓到它,cargo clippy -- -W clippy::let_underscore_must_use在token_let上报「non-bindingleton an expression with#[must_use]type」,note: type is std::result::Result。 - 在芯片上,这个 panic 让整个程序停在 panic 处理函数里;panic 处理函数能打印位置时(比如 QEMU 实验用的
panic-semihosting),看到spawner.rs:54就回到本页。
小结
- 调用任务函数就占了一格;令牌被 drop 时
Droppanic:SpawnToken instances may not be dropped。 - 常见来路:令牌创建之后的
?提前返回,把真正的错误盖掉;let _ = task(),恰好是 rustc 的help建议的写法。 - 先做完会失败的事,再
spawner.spawn(task(..)?),令牌不落进变量。