T-G: zy369369在同步的 Rust 方法中调用异步代码经常会导致一些问题,特别是对于不熟悉异步 Rust runtime 底层原理的初学者。本人网站有详细介绍多种方法http://www.jizhifuke.com
在做 GreptimeDB 项目的时候,我们遇到一个关于在同步 Rust 方法中调用异步代码的问题。经过一系列故障排查后,我们弄清了问题的原委,这大大加深了对异步 Rust 的理解,因此在这篇文章中分享给大家,希望能给被相似问题困扰的 Rust 开发者一些启发
我们的整个项目是基于 Tokio 这个异步 Rust runtime 的,它将协作式的任务运行和调度方便地封装在 .await 调用中,非常简洁优雅。但是这样也让不熟悉 Tokio 底层原理的用户一不小心就掉入到坑里。
我们遇到的问题是,需要在一个第三方库的 trait 实现中执行一些异步代码,而这个 trait 是同步的,我们无法修改这个 trait 的定义。
trait Sequencer {
fn generate(&self) -> Vec
}
我们用一个 PlainSequencer 来实现这个 trait ,而在实现 generate 方法的时候依赖一些异步的调用(比如这里的 PlainSequencer::generate_async):
impl PlainSequencer {
async fn generate_async(&self)->Vec
let mut res = vec![];
for i in 0..self.bound {
res.push(i);
tokio::time::sleep(Duration::from_millis(100)).await;
}
res
}
}
impl Sequencer for PlainSequencer {
fn generate(&self) -> Vec
self.generate_async().await
}
}
这样就会出现问题,因为 generate 是一个同步方法,里面是不能直接 await 的。
error[E0728]: await
is only allowed inside async
functions and blocks
--> src/common/tt.rs:32:30
|
31 | / fn generate(&self) -> Vec
32 | | self.generate_async().await
| | ^^^^^^ only allowed inside async
functions and blocks
33 | | }
| |_____- this is not async
我们首先想到的是,Tokio 的 runtime 有一个 Runtime::block_on 方法,可以同步地等待一个 future 完成。
impl Sequencer for PlainSequencer {
fn generate(&self) -> Vec
RUNTIME.block_on(async{
self.generate_async().await
})
}
}
[cfg(test)]
mod tests {
#[tokio::test]
async fn test_sync_method() {
let sequencer = PlainSequencer {
bound: 3
};
let vec = sequencer.generate();
println!("vec: {:?}", vec);
}
}
编译可以通过,但是运行时直接报错:
Cannot start a runtime from within a runtime. This happens because a function (like block_on
) attempted to block the current thread while the thread is being used to drive asynchronous tasks.
thread 'tests::test_sync_method' panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like block_on
) attempted to block the current thread while the thread is being used to drive asynchronous tasks.', /Users/lei/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.17.0/src/runtime/enter.rs:39:9
提示不能从一个执行中的 runtime 直接启动另一个异步 runtime。看来 Tokio 为了避免这种情况特地在 Runtime::block_on 入口做了检查。既然不行那我们就再看看其他的异步库是否有类似的异步转同步的方法。
果然找到一个 futures::executor::block_on。
impl Sequencer for PlainSequencer {
fn generate(&self) -> Vec
futures::executor::block_on(async {
self.generate_async().await
})
}
}
编译同样没问题,但是运行时代码直接直接 hang 住不返回了。
cargo test --color=always --package tokio-demo \
--bin tt tests::test_sync_method \
--no-fail-fast -- --format=json \
--exact -Z unstable-options --show-output
Compiling tokio-demo v0.1.0 (/Users/lei/Workspace/Rust/learning/tokio-demo)
Finished test [unoptimized + debuginfo] target(s) in 0.39s
Running unittests src/common/tt.rs (target/debug/deps/tt-adb10abca6625c07)
{ "type": "suite", "event": "started", "test_count": 1 }
{ "type": "test", "event": "started", "name": "tests::test_sync_method" }