Rust in Everything
基于 embassy-time 0.5.1

Found waker not created by the Embassy executor

主线位置:定时器主线的「进定时器队列」。集成队列的节点就在任务头里,驱动要从 cx.waker() 找回任务,见定时器队列的「从 waker 找回任务」;block_on 的例子在 embassy-futures 里出现过。本页把会造出「别人的 waker」的几种写法放在一起。

flowchart TB
  t["Timer poll"] --> sw["schedule_wake at waker"]
  sw --> q{"哪种队列"}
  q -->|集成| f["task_from_waker"]
  f --> v{"vtable 是 embassy 的"}
  v -->|是| node["任务头里的节点"]
  v -->|不是| p["panic"]
  q -->|"generic-queue-N"| arr["定长数组存 waker"]

实验 crate exp-embassy/pita-std,bin foreign、foreign_fixed,和 exp-embassy/pita-genq,bin foreign_genq(platform-std,embassy-time 开 std,debug);M2 Max、macOS 27、rustc 1.99.0。

现象

同一个 10 ms 的 Timer,三种 poll 它的方式(foreign <mode>):

#[embassy_executor::task]
async fn unordered() {
  // inside an embassy task, but FuturesUnordered gives each future its own waker
  let mut set = FuturesUnordered::new();
  set.push(Timer::after_millis(10));
  set.next().await;
  println!("unordered: done at {} ms", Instant::now().as_millis());
}

// main, by mode:
//   block_on:  embassy_futures::block_on(Timer::after_millis(10));
//   tokio:     tokio current_thread runtime, rt.block_on(async { Timer::after_millis(10).await });
//   unordered: spawner.spawn(unordered().unwrap());

三种都是同样的两段 panic,退出码 101(block_on 的输出,另两种只有线程号不同):

thread 'main' (5060979) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-executor-0.10.0/src/raw/waker.rs:35:5:
unwrap of `try_task_from_waker(waker)` failed: Found waker not created by the Embassy executor. Unless the generic timer queue is enabled, `embassy_time::Timer` only works with the Embassy executor.: NoneError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread '<unnamed>' (5060980) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-time-0.5.1/src/driver_std.rs:54:36:
called `Result::unwrap()` on an `Err` value: PoisonError { .. }
  • 前两种好理解:block_on 和 tokio 用的都是自己的 waker。
  • 第三种最容易踩:代码明明跑在 embassy 任务里。FuturesUnordered 为了知道是哪个子 future 被叫醒,给每个子 future 造一个自己的 waker(内部的 Arc),Timer 拿到的就是它。futures::stream::FuturesOrdered 和 FutureExt::shared() 也给子 future 换 waker,bin foreign_more 在 embassy 任务里各试一次,是同一条 panic。
  • 第二个 panic 是连带的:第一个发生在 std 驱动持锁时,锁被毒化,驱动的闹钟线程跟着 panic(定时器队列)。

出处

Timer::poll 调 schedule_wake(at, cx.waker()),std 驱动转给集成队列的 Queue::schedule_wake,第一步是 TimerQueueItem::from_embassy_waker(waker),它调执行器导出的 __embassy_time_queue_item_from_waker,后者调 task_from_waker:

pub fn task_from_waker(waker: &Waker) -> TaskRef {
    unwrap!(
        try_task_from_waker(waker),
        "Found waker not created by the Embassy executor. Unless the generic timer queue is enabled, `embassy_time::Timer` only works with the Embassy executor."
    )
}

pub(crate) fn try_task_from_waker(waker: &Waker) -> Option<TaskRef> {
    // make sure to compare vtable addresses. Doing `==` on the references
    // will compare the contents, which is slower.
    if waker.vtable() as *const _ != &VTABLE as *const _ {
        return None;
    }
    // safety: our wakers are always created with `TaskRef::as_ptr`
    Some(unsafe { TaskRef::from_ptr(waker.data() as *const TaskHeader) })
}

判断只看 vtable 地址。embassy 执行器的 waker 数据指针就是 TaskRef,节点在任务头里,所以拿到 waker 就能找到节点;别的 waker 指向别的东西,没有节点可用。FuturesUnordered 包了一层,vtable 是 futures-util 的,于是即使外面是 embassy 任务也认不出来。

📝NOTE
冷知识:报错里的「Unless the generic timer queue is enabled」是 0.10.0 才加的

0.9.1 的 task_from_waker 直接 panic!("Found waker not created by the Embassy executor. `embassy_time::Timer` only works with the Embassy executor."),没有提示出路。#5100 的提交「executor: Add fallible from_waker getter」把判断拆成返回 Option 的 try_task_from_waker,原函数改成在它外面 unwrap!,消息补上了通用队列这条路;同时新导出一个不 panic 的 __try_embassy_time_queue_item_from_waker。首次进入 embassy-executor 0.10.0,CHANGELOG 记为「Added __try_embassy_time_queue_item_from_waker」和「Added fallible from_waker getter for TaskRef」。锁定的 embassy-time-queue-utils 0.3.2 还没有调用这个 __try_ 符号,所以本页的 panic 照旧;main 上的 embassy-executor-timer-queue 已经在用它(未发版)。

修法

在 embassy 任务里,用传递原 waker 的组合子。 「几个定时器谁先到」用 embassy_futures::select::select_array(foreign_fixed):

#[embassy_executor::task]
async fn first_of() {
  let (_, i) = select_array([Timer::after_millis(30), Timer::after_millis(10)]).await;
  println!("select_array: timer {i} first, at {} ms", Instant::now().as_millis());
}
select_array: timer 1 first, at 12 ms

select / join 系列把任务自己的 cx 原样传给每个子 future(embassy-futures),一个任务里两个定时器共用任务头里那一个节点,节点取最早的时刻(定时器队列)。

要在别的执行器、block_on 或测试里用 Timer,打开通用队列。 pita-genq 只是给 embassy-time 多开一个 generic-queue-8,同一份 foreign.rs(foreign_genq <mode>):

block_on: done
tokio: done
unordered: done at 10 ms

三种都正常。通用队列是一个定长数组,直接存 waker,不需要从 waker 反查任务。代价有两个:所有任务的定时器挤在 N 个格子里,格子满了不会报错而是提前唤醒、忙转(通用定时器队列满了:不报错,忙转);embassy-time 的 Cargo.toml 也写明库不要开 generic-queue-*,留给二进制选。

反方向(在 embassy 任务里用 tokio 的类型)是另一个坑,见 there is no reactor running。

怎么确认没中招

  • 用到 embassy-time 的代码路径上,搜会换 waker 的组合子:grep -rn 'FuturesUnordered\|FuturesOrdered\|\.shared()\|block_on' src/。
  • 测试代码里常用 futures::executor::block_on 或 #[tokio::test] 驱动 async 代码,碰到 Timer 就是这条 panic。测试要么跑在 embassy 的 std 执行器上,要么在测试的 feature 里开 generic-queue-*。
  • 看到 PoisonError 出自 driver_std.rs:54 时,往上找第一个 panic,它才是原因。

小结

  • 集成定时器队列靠 waker 的 vtable 认出 embassy 任务,认不出就 panic:Found waker not created by the Embassy executor。
  • block_on、tokio 运行时,以及 embassy 任务里的 FuturesUnordered 都会给 Timer 一个别人的 waker。
  • embassy 任务里用 embassy-futures 的 select / join;必须跨执行器时开 generic-queue-N,并接受它的容量限制。

💬 关联讨论

发起讨论
加载中...