there is no reactor running, must be called from the context of a Tokio 1.x runtime
主线位置:定时器主线和 I/O 唤醒的「谁来叫醒」这一环。embassy 的任务由 embassy 的时间驱动和中断叫醒;tokio 的套接字和定时器由 tokio 的驱动叫醒。两者为什么接不上、能接的部分有多少,见本站的定时循环搬到 embassy;tokio 一侧的同一个报错见 tokio 卷的 there is no reactor running。
flowchart TB
task["embassy 任务"] --> io["tokio TcpListener::bind"]
io --> h{"当前线程有 tokio 句柄吗"}
h -->|没有| p["panic no reactor"]
th["tokio 线程 block_on"] --> work["tokio I/O"]
work --> ch["embassy-sync Channel send"]
ch --> wake["叫醒 embassy 任务"]
实验 crate exp-embassy/pitb-std,bin p28_tokio(platform-std 执行器、tokio 1.54.1,release);M2 Max、macOS 27、rustc 1.99.0。
现象
一段从 tokio 项目里搬来的函数:在回环上起一个服务、连上去、读一行欢迎语。所有类型都是 tokio 的,在 embassy 任务里调用:
async fn fetch_banner() -> String {
// stands in for "talk to some server": a loopback echo, all tokio types
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (mut s, _) = listener.accept().await.unwrap();
s.write_all(b"220 hello").await.unwrap();
});
let mut s = tokio::net::TcpStream::connect(addr).await.unwrap();
let mut buf = String::new();
s.read_to_string(&mut buf).await.unwrap();
buf
}
#[embassy_executor::task]
async fn direct() {
let banner = fetch_banner().await;
println!("direct: {banner}");
}
能编译。运行:
thread 'main' (5056038) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.54.1/src/net/tcp/listener.rs:304:22:
there is no reactor running, must be called from the context of a Tokio 1.x runtime
tokio::time::sleep 同样在创建时 panic(本站的定时循环搬到 embassy 里有原文);tokio::spawn 也要当前运行时。
出处
TcpListener::new 把套接字登记进 tokio 的 I/O 驱动(PollEvented::new),要先从线程局部的上下文里取当前运行时的句柄;embassy 执行器的线程上从来没有进入过任何 tokio 运行时,取不到就 panic,消息是 CONTEXT_MISSING_ERROR。
这不是「embassy 不兼容 Future」。embassy 执行器能 poll 任何 Future,问题在于谁来叫醒它:tokio 的 TcpStream 在 Pending 时把 waker 交给 tokio 的 I/O 驱动(kqueue / epoll),tokio 的 Sleep 交给 tokio 的时间轮。这些驱动只在 tokio 的工作线程 park 时运转(tokio 卷驱动栈与 park)。embassy 的 std 执行器空闲时等的是自己的条件变量,被 embassy 的时间驱动和 __pender 叫醒(各平台的执行器),不会去转 tokio 的驱动。tokio 在创建时就检查上下文,效果是这种 future 不会在没有驱动的地方静静地挂起,而是立刻 panic。
修法
tokio 的 I/O 留在 tokio 里:另起一个线程跑 tokio 运行时,结果经 embassy-sync 的 Channel 交给 embassy 任务。Channel 用 CriticalSectionRawMutex(std 上 critical-section 是一把全局锁),可以跨线程 send;send 叫醒等在 receive 上的 embassy 任务,embassy 执行器的 __pender 把它的线程唤醒(p28_tokio fix):
static REPLIES: Channel<CriticalSectionRawMutex, String, 4> = Channel::new();
#[embassy_executor::task]
async fn bridged() {
let t0 = StdInstant::now();
let banner = REPLIES.receive().await;
println!("bridged: {banner:?} after {:?}, on thread {:?}", t0.elapsed(), std::thread::current().name());
std::process::exit(0);
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
std::thread::Builder::new()
.name("tokio".into())
.spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
rt.block_on(async {
let banner = fetch_banner().await;
REPLIES.send(banner).await; // wakes the embassy task from the tokio thread
});
})
.unwrap();
spawner.spawn(bridged().unwrap());
}
三次:
bridged: "220 hello" after 366.333µs, on thread Some("main")
bridged: "220 hello" after 289.458µs, on thread Some("main")
bridged: "220 hello" after 330.791µs, on thread Some("main")
fetch_banner 一字未改,在名为 tokio 的线程上由 tokio 驱动;结果在 main 线程(embassy 执行器)上被 embassy 任务收到。反方向(embassy 任务给 tokio 发命令)同理,tokio 一侧 .await 一个 embassy-sync 的 receive() 也行,它只需要 tokio 的 waker 被调用。
另一种写法是在启动 embassy 执行器之前 rt.enter() 进入一个多线程 tokio 运行时的上下文、但不 block_on:tokio 的 future 能创建了,驱动在 tokio 的工作线程上转。换成 current_thread 运行时就没有线程去转驱动,future 永远挂起,见本站的定时循环搬到 embassy 的动脑筋。这种写法把两个运行时绑在同一段代码里,出错时更难查,不如上面的通道清楚。
这个坑只出在 std 上:tokio 需要 std 和操作系统的事件机制,thumbv7em-none-eabihf 这类裸机目标上用不了它。要在 embassy 里联网,用 embassy-net。
怎么确认没中招
- panic 消息是
there is no reactor running,位置在tokio-1.x/src/...:看调用栈里是不是 embassy 任务。 grep -rn 'tokio::' src/后看每一处所在的函数最终是被#[embassy_executor::task]还是 tokio 运行时 poll。- 通过
Channel跨线程时RawMutex必须是CriticalSectionRawMutex;用NoopRawMutex的static直接编不过(见*mut ()cannot be shared between threads safely)。
小结
- tokio 的套接字、定时器、
spawn创建时就要求当前线程在 tokio 运行时里,embassy 任务里调用直接 panic「there is no reactor running」。 - embassy 执行器能 poll 任何 future,但叫醒 tokio future 的是 tokio 的驱动,embassy 不会去转它。
- 修法:tokio 在自己的线程里
block_on,结果用Channel<CriticalSectionRawMutex, ..>交给 embassy 任务。