Rust in Everything
基于 embassy-executor 0.10.0

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。

出处

Drop for SpawnToken:

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-binding let on an expression with #[must_use] type」,note: type is std::result::Result。
  • 在芯片上,这个 panic 让整个程序停在 panic 处理函数里;panic 处理函数能打印位置时(比如 QEMU 实验用的 panic-semihosting),看到 spawner.rs:54 就回到本页。

小结

  • 调用任务函数就占了一格;令牌被 drop 时 Drop panic:SpawnToken instances may not be dropped。
  • 常见来路:令牌创建之后的 ? 提前返回,把真正的错误盖掉;let _ = task(),恰好是 rustc 的 help 建议的写法。
  • 先做完会失败的事,再 spawner.spawn(task(..)?),令牌不落进变量。

💬 关联讨论

发起讨论
加载中...