Rust in Everything
基于 embassy-executor 0.10.0

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 任务。

💬 关联讨论

发起讨论
加载中...