Rust in Everything
基于 tokio 1.54.1

there is no reactor running

主线位置:任务主线的第一步,tokio::spawn 从线程本地上下文取当前运行时的 Handle。Sleep、TcpStream 等类型在构造时也要走这一步。

完整报错:

there is no reactor running, must be called from the context of a Tokio 1.x runtime
flowchart TB
  call["spawn / sleep / TcpStream"] --> cur{线程本地有句柄?}
  cur -->|没有| p1["panic:no reactor running"]
  cur -->|有| drv{对应驱动开了?}
  drv -->|没有| p2["panic:timers / IO disabled"]
  drv -->|开了| ok[正常]

触发它的代码

最常见的一种:在运行时里起了一个普通线程,在那个线程上调 tokio::spawn。

fn main() {
  let rt = tokio::runtime::Runtime::new().unwrap();
  rt.block_on(async {
    // a plain OS thread knows nothing about the runtime above
    std::thread::spawn(|| {
      tokio::spawn(async { println!("never runs") });
    })
    .join()
    .unwrap_err();
  });
}
thread '<unnamed>' (15636697) panicked at src/bin/nr_thread.rs:6:7:
there is no reactor running, must be called from the context of a Tokio 1.x runtime
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

外层确实有一个运行时,但「当前运行时」记在线程本地变量里,新线程的那一份是空的。

📝NOTE
没有蠢问题

问: 我的 main 上标着 #[tokio::main],报错的调用也就在 main 线程上,为什么还是这句?

答: 先看依赖树里有没有两个大版本的 tokio(cargo tree -d)。0.2、0.3 和 1.x 对 Cargo 来说是不同的 crate,各有各的线程本地变量 CONTEXT。消息里写的是「Tokio 1.x runtime」:发出它的是 1.x 的代码,它只看 1.x 的那份变量。如果外层运行时来自另一个大版本(比如依赖了一个还在用 tokio 0.2 的老框架),1.x 那份就是空的,等于没有运行时。反过来也一样:1.x 的运行时里调用基于 0.2 的库,panic 的是 0.2 的代码。

其他几种触发方式放在一起跑(quiet_hook 只把 panic 消息打印出来,case 用 catch_unwind 接住 panic):

fn main() {
  quiet_hook();

  case("1. futures::executor::block_on + tokio::time::sleep", || {
    futures::executor::block_on(async {
      tokio::time::sleep(Duration::from_millis(1)).await;
    });
  });

  case("2. futures::executor::block_on + TcpStream::connect", || {
    futures::executor::block_on(async {
      let _ = tokio::net::TcpStream::connect("127.0.0.1:9").await;
    });
  });

  let rt = tokio::runtime::Runtime::new().unwrap();
  case("3. rt.block_on(sleep(..)): Sleep built before block_on", || {
    rt.block_on(tokio::time::sleep(Duration::from_millis(1)));
  });

  case("4. TcpStream::from_std on a plain thread", || {
    let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let s = std::net::TcpStream::connect(l.local_addr().unwrap()).unwrap();
    s.set_nonblocking(true).unwrap();
    let _ = tokio::net::TcpStream::from_std(s);
  });

  let bare = tokio::runtime::Builder::new_current_thread().build().unwrap();
  case("5. runtime without enable_time: sleep", || {
    bare.block_on(async { tokio::time::sleep(Duration::from_millis(1)).await });
  });
  case("6. runtime without enable_io: TcpListener::bind", || {
    bare.block_on(async {
      let _ = tokio::net::TcpListener::bind("127.0.0.1:0").await;
    });
  });
}
1. futures::executor::block_on + tokio::time::sleep
  panic: there is no reactor running, must be called from the context of a Tokio 1.x runtime
2. futures::executor::block_on + TcpStream::connect
  panic: there is no reactor running, must be called from the context of a Tokio 1.x runtime
3. rt.block_on(sleep(..)): Sleep built before block_on
  panic: there is no reactor running, must be called from the context of a Tokio 1.x runtime
4. TcpStream::from_std on a plain thread
  panic: there is no reactor running, must be called from the context of a Tokio 1.x runtime
5. runtime without enable_time: sleep
  panic: A Tokio 1.x context was found, but timers are disabled. Call `enable_time` on the runtime builder to enable timers.
6. runtime without enable_io: TcpListener::bind
  panic: A Tokio 1.x context was found, but IO is disabled. Call `enable_io` on the runtime builder to enable IO.
情况原因
1、2:别的执行器(futures::executor、async-std、smol 等)它们不设置 tokio 的线程本地句柄。tokio 的定时器和 IO 类型离不开 tokio 的驱动
3:rt.block_on(sleep(d))sleep(d) 作为参数先求值,这时还没进 block_on
4:from_std 在普通线程上from_std 是同步函数,调用时立刻向反应器注册
5、6:找到了运行时,但 Builder 没开对应驱动报错换成另外两句,直接告诉你缺哪个 enable_*

源码里在哪

报错文本是常量 CONTEXT_MISSING_ERROR。抛出它的是 scheduler::Handle::current:

pub(crate) fn current() -> Handle {
    match context::with_current(Clone::clone) {
        Ok(handle) => handle,
        Err(e) => panic!("{}", e),
    }
}

它读线程本地变量 CONTEXT 里的 current 字段,读不到就 panic。这个字段谁在什么时候写、为什么消息里说的是 reactor 而实际查的是整个运行时,见线程本地上下文。

📝NOTE
冷知识:这句报错是 1.1.0 统一出来的

1.0 里,「找不到运行时」有好几种说法:tokio::spawn 报 must be called from the context of Tokio runtime configured with either basic_scheduler or threaded_scheduler,Handle::current 报 not currently running on the Tokio runtime.,IO 和定时器各报一句 there is no reactor running ... / there is no timer running ...。issue #3361 提出,这些消息最好一样,至少有一段相同的文字,「这样搜索和找到解决办法更容易」,附的链接是一个 Stack Overflow 问题:为什么明明写了 #[tokio::main] 还报 there is no reactor running。

#3441(1.1.0)加了常量 CONTEXT_MISSING_ERROR,所有找不到上下文的地方都改用它。它沿用了 IO 那句的开头,所以连 spawn 报的也是「no reactor」。同一个 PR 把「找到了上下文、但驱动没开」分了出来,就是上面情况 5、6 的 A Tokio 1.x context was found, but ... 两句。

调它的地方不只是 spawn。凡是要挂到驱动上的类型,构造时都会取一次句柄:

取到句柄以后,再从句柄里拿驱动。驱动是 Option,没开就是 None,expect 给出另外两句报错:driver::Handle::io 和 driver::Handle::time。Builder::new_* 默认什么驱动都不开,#[tokio::main] 展开时会加 enable_all()(见从 #[tokio::main] 开始),所以手写 Builder 的人更容易碰到这两句。

怎么修

在运行时里构造

情况 3 只要让 sleep 晚一点构造:

rt.block_on(async { tokio::time::sleep(d).await });

async 块是惰性的,sleep(d) 要等 block_on 第一次 poll 它才执行,那时已经在运行时里了。

把 Handle 传过去

Handle 是 Clone + Send,可以在进运行时以后取一份,带进别的线程。在那个线程上调 Handle 自己的方法,不依赖线程本地变量:

let handle = Handle::current();
let t = std::thread::spawn(move || {
  let jh = handle.spawn(async { "spawned via handle.spawn" });
  handle.block_on(jh).unwrap()
});

Handle::enter

要调的代码不是你写的(库函数里直接调了 tokio::spawn),没法改成 handle.spawn,就在那个线程上「进入」运行时:

let handle = Handle::current();
let t = std::thread::spawn(move || {
  let _guard = handle.enter();
  let jh = tokio::spawn(async {
    tokio::time::sleep(Duration::from_millis(10)).await;
    "tokio::spawn + sleep after enter()"
  });
  handle.block_on(jh).unwrap()
});

Handle::enter 只是把句柄写进线程本地变量,guard drop 时恢复原值。它不让这个线程去跑任务:任务还是由运行时自己的线程执行。

从别的执行器调用

把需要 tokio 的那部分交给 tokio 运行时,在别的执行器里 await 它的 JoinHandle。JoinHandle 是普通的 future,唤醒走标准的 Waker,哪个执行器都能 await:

let handle = Handle::current();
let v = std::thread::spawn(move || {
  futures::executor::block_on(async move {
    handle
      .spawn(async {
        tokio::time::sleep(Duration::from_millis(10)).await;
        42
      })
      .await
      .unwrap()
  })
})
.join()
.unwrap();

上面三段放在一个 #[tokio::main] 里运行:

A: spawned via handle.spawn
B: tokio::spawn + sleep after enter()
C: futures executor awaited a tokio task -> 42

开驱动

报的是 timers are disabled 或 IO is disabled,就在 Builder 上加 enable_time()、enable_io(),或者直接 enable_all()。

current_thread 的一个陷阱

上面的修法在多线程运行时上都没问题,因为 worker 线程一直在跑任务。current_thread 运行时只有调 block_on 的那个线程能跑任务。如果这个线程正阻塞着等别的线程,别的线程 enter 之后 spawn 的任务就没人执行:

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let handle = Handle::current();
  let t = std::thread::spawn(move || {
    let _guard = handle.enter();
    let jh = tokio::spawn(async { "ran" });
    // the only thread that can run tasks is blocked in join() below
    std::thread::sleep(Duration::from_millis(500));
    println!("task finished after 500ms? {}", jh.is_finished());
  });
  t.join().unwrap(); // blocks the current_thread runtime
  tokio::task::yield_now().await;
  println!("after join + yield");
}
task finished after 500ms? false
after join + yield

不 panic,只是不跑。如果那个线程接着 handle.block_on(jh),就是死锁。

选哪个

情况改法
rt.block_on(sleep(..))、在 async 外面构造了 tokio 类型包进 async { .. },挪到运行时里构造
自己起的线程,自己写的代码传 Handle,调 handle.spawn / handle.block_on
自己起的线程,调的是会 tokio::spawn 的库let _g = handle.enter();
主程序用别的执行器单独建一个 tokio 运行时,用 handle.spawn(..) 把 tokio 部分交给它
timers are disabled / IO is disabledBuilder 上 enable_all()

小结

  • 这句报错只有一个意思:当前线程的线程本地变量里没有 tokio 运行时句柄。外层有没有运行时不重要,要看的是这个线程。
  • spawn、sleep、interval、timeout、各种 socket 的构造都会取句柄;返回 future 的函数在第一次 poll 时才取,同步构造函数在调用时就取。
  • 找到了运行时但没开驱动,报的是 timers are disabled / IO is disabled,按提示加 enable_*。
  • 跨线程用 Handle:要么调它的方法,要么 enter() 之后调全局函数。在 current_thread 上,还要保证有线程在 block_on 里跑任务。

💬 关联讨论

发起讨论
加载中...