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,binforeign_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 任务也认不出来。
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,并接受它的容量限制。