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
外层确实有一个运行时,但「当前运行时」记在线程本地变量里,新线程的那一份是空的。
问: 我的 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 而实际查的是整个运行时,见线程本地上下文。
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。凡是要挂到驱动上的类型,构造时都会取一次句柄:
Sleep::new_timeout:sleep()、sleep_until()、timeout()、interval()都经过它。PollEvented::new_with_interest:TcpStream、TcpListener、UdpSocket、UnixStream等向反应器注册时经过它。
取到句柄以后,再从句柄里拿驱动。驱动是 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 disabled | Builder 上 enable_all() |
小结
- 这句报错只有一个意思:当前线程的线程本地变量里没有 tokio 运行时句柄。外层有没有运行时不重要,要看的是这个线程。
spawn、sleep、interval、timeout、各种 socket 的构造都会取句柄;返回 future 的函数在第一次 poll 时才取,同步构造函数在调用时就取。- 找到了运行时但没开驱动,报的是
timers are disabled/IO is disabled,按提示加enable_*。 - 跨线程用
Handle:要么调它的方法,要么enter()之后调全局函数。在 current_thread 上,还要保证有线程在block_on里跑任务。