网易首页 > 网易号 > 正文 申请入驻

如何在同步的Rust方法中调用异步代码 | Tokio使用中的几点教训

0
分享至

在同步的 Rust 方法中调用异步代码经常会导致一些问题,特别是对于不熟悉异步 Rust runtime 底层原理的初学者。在本文中,我们将讨论我们遇到的一个特殊问题,并分享我们采取的解决方法的经验。
背景和问题

在做 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" }

# the execution just hangs here :(

明明generate_async方法里面只有一个简单的sleep()调用,但是为什么 future 一直没完成呢?

并且吊诡的是,同样的代码,在tokio::test里面会 hang 住,但是在tokio::main中则可以正常执行完毕:

#[tokio::main]
pub async fn main() {
let sequencer = PlainSequencer {
bound: 3
let vec = sequencer.generate();
println!("vec: {:?}", vec);

执行结果:

cargo run --color=always --package tokio-demo --bin tt
Finished dev [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/tt`
vec: [0, 1, 2]

其实当初真正遇到这个问题的时候定位到具体在哪里 hang 住并没有那么容易。真实代码中 async 执行的是一个远程的 gRPC 调用,当初怀疑过是否是 gRPC server 的问题,动用了网络抓包等等手段最终发现是 client 侧的问题。 这也提醒了我们在出现 bug 的时候,抽象出问题代码的执行模式并且做出一个最小可复现的样例(Minimal Reproducible Example)是非常重要的。
Catchup

在 Rust 中,一个异步的代码块会被make_async_expr编译为一个实现了std::future::Future的 generator。

#[tokio::test]
async fn test_future() {
let future = async{
println!("hello");

// the above async block won't get executed until we await it.
future.await;
}

.await本质上是一个语法糖,则会被lower_expr_await编译成类似于下面的一个语法结构:

// pseudo-rust code
match ::std::future::IntoFuture::into_future() {
mut __awaitee => loop {
match unsafe { ::std::future::Future::poll(
<::std::pin::Pin>::new_unchecked(&mut __awaitee),
::std::future::get_context(task_context),
::std::task::Poll::Ready(result) => break result,
::std::task::Poll::Pending => {}
task_context = yield ();

在上面这个去掉了语法糖的伪代码中,可以看到有一个循环不停地检查 generator 的状态是否为已完成(std::future::Future::poll)。

自然地,必然存在一个组件来做这件事,这里就是 Tokio 和 async-std 这类异步运行时发挥作用的地方了。Rust 在设计之初就特意将异步的语法(async/await)和异步运行时的实现分开,在上述的示例代码中,poll 的操作是由 Tokio 的 executor 执行的。

问题分析

回顾完背景知识,我们再看一眼方法的实现:

fn generate(&self) -> Vec {
futures::executor::block_on(async {
self.generate_async().await

调用generate方法的肯定是 Tokio 的 executor,那么 block_on 里面的self.generate_async().await这个 future 又是谁在 poll 呢?

一开始我以为,futures::executor::block_on会有一个内部的 runtime 去负责generate_async的 poll。于是查看了代码(主要是futures_executor::local_pool::run_executor这个方法):

fn run_executorFnMut(&mut Context<'_>) -> Poll>(mut f: F) -> T {
let _enter = enter().expect(
"cannot execute `LocalPool` executor from within \
another executor",

CURRENT_THREAD_NOTIFY.with(|thread_notify| {
let waker = waker_ref(thread_notify);
let mut cx = Context::from_waker(&waker);
loop {
if let Poll::Ready(t) = f(&mut cx) {
return t;
}
let unparked = thread_notify.unparked.swap(false, Ordering::Acquire);
if !unparked {
thread::park();
thread_notify.unparked.store(false, Ordering::Release);
}
}
})
}

立刻嗅到了一丝不对的味道,虽然这个方法名为run_executor,但是整个方法里面貌似没有任何 spawn 的操作,只是在当前线程不停的循环判断用户提交的 future 的状态是否为 ready 啊!

这意味着,当 Tokio 的 runtime 线程执行到这里的时候,会立刻进入一个循环,在循环中不停地判断用户的的 future 是否 ready。如果还是 pending 状态,则将当前线程 park 住。

假设,用户 future 的异步任务也是交给了当前线程去执行,futures::executor::block_on等待用户的 future ready,而用户 future 等待futures::executor::block_on释放当前的线程资源,那么不就死锁了?

这个推论听起来很有道理,让我们来验证一下。既然不能在当前 runtime 线程 block,那就重新开一个 runtime block:

impl Sequencer for PlainSequencer {
fn generate(&self) -> Vec {
let bound = self.bound;
futures::executor::block_on(async move {
RUNTIME.spawn(async move {
let mut res = vec![];
for i in 0..bound {
res.push(i);
tokio::time::sleep(Duration::from_millis(100)).await;
res
}).await.unwrap()

果然可以了。

cargo test --color=always --package tokio-demo \

--bin tt tests::test_sync_method \

--no-fail-fast -- --format=json \

--exact -Z unstable-options --show-output

Finished test [unoptimized + debuginfo] target(s) in 0.04s
Running unittests src/common/tt.rs (target/debug/deps/tt-adb10abca6625c07)
vec: [0, 1, 2]

值得注意的是,在futures::executor::block_on里面,额外使用了一个RUNTIME来 spawn 我们的异步代码。其原因还是刚刚所说的,这个异步任务需要一个 runtime 来驱动状态的变化。

如果我们删除RUNTIME,而为futures::executor::block_on生成一个新的线程,虽然死锁问题得到了解决,但tokio::time::sleep方法的调用会报错"no reactor is running",这是因为 Tokio 的功能运作需要一个 runtime:

called `Result::unwrap()` on an `Err` value: Any { .. }
thread '' panicked at 'there is no reactor running, must be called from the context of a Tokio 1.x runtime',
tokio::maintokio::test

在分析完上面的原因之后,“为什么tokio::main中不会 hang 住而tokio::test会 hang 住?“ 这个问题也很清楚了,他们两者所使用的的 runtime 并不一样。tokio::main使用的是多线程的 runtime,而tokio::test使用的是单线程的 runtime,而在单线程的 runtime 下,当前线程被futures::executor::block_on卡死,那么用户提交的异步代码是一定没机会执行的,从而必然形成上面所说的死锁。

Best practice

经过上面的分析,结合 Rust 基于 generator 的协作式异步特性,我们可以总结出 Rust 下桥接异步代码和同步代码的一些注意事项:

  • • 将异步代码与同步代码结合使用可能会导致阻塞,因此不是一个明智的选择。

  • • 在同步的上下文中调用异步代码时,请使用futures::executor::block_on并将异步代码 spawn 到另一个专用的 runtime 中执行 ,因为前者会阻塞当前线程。

  • • 如果必须从异步的上下文中调用有可能阻塞的同步代码(比如文件 IO 等),则建议使用tokio::task::spawn_blocking在专门处理阻塞操作的 executor 上执行相应的代码。


参考
  • • Async: What is blocking?

  • https://ryhl.io/blog/async-what-is-blocking/

  • • Generators and async/await

  • https://cfsamson.github.io/books-futures-explained/4_generators_async_await.html

  • • Async and Await in Rust: a full proposal

  • https://news.ycombinator.com/item?id=17536441

  • • calling futures::executor::block_on in block_in_place may hang

  • https://github.com/tokio-rs/tokio/issues/2603

  • • tokio@0.2.14 + futures::executor::block_on causes hang

  • https://github.com/tokio-rs/tokio/issues/2376

以上是我们在构建 GreptimeDB 过程中遇到关于同步/异步代码相互调用的一些思考和经验,希望能给正在相关领域努力的伙伴们一些启发。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
快讯!王思聪冲上热搜!

快讯!王思聪冲上热搜!

达文西看世界
2026-04-27 15:21:26
埃贝尔:孔帕尼会体验到我坐在看台上的感觉;巴黎和拜仁很像

埃贝尔:孔帕尼会体验到我坐在看台上的感觉;巴黎和拜仁很像

懂球帝
2026-04-28 00:30:07
林志玲一家三口吃饭,穿着拖鞋抱着孩子妈味很足,4岁儿子好黏她

林志玲一家三口吃饭,穿着拖鞋抱着孩子妈味很足,4岁儿子好黏她

赏心悦目的我
2026-04-28 01:17:50
醪糟再次被关注!医生发现:高血脂患者喝醪糟,不用多久4大变化

醪糟再次被关注!医生发现:高血脂患者喝醪糟,不用多久4大变化

芹姐说生活
2026-04-19 15:52:53
5月1日新规落地!3项收费正式全面取消,老百姓再也不用乱花钱

5月1日新规落地!3项收费正式全面取消,老百姓再也不用乱花钱

复转这些年
2026-04-26 17:11:23
历史老师跌入“无人区”:某高中20人教研组,近一半无学生可教

历史老师跌入“无人区”:某高中20人教研组,近一半无学生可教

听心堂
2026-03-31 15:52:04
iPhone Ultra折叠屏尺寸图曝光:折叠厚度为9.23mm

iPhone Ultra折叠屏尺寸图曝光:折叠厚度为9.23mm

CNMO科技
2026-04-27 15:59:12
梅西跨洋督战:麾下球队科尔内利亚告负,球王远在迈阿密发文打气

梅西跨洋督战:麾下球队科尔内利亚告负,球王远在迈阿密发文打气

星耀国际足坛
2026-04-27 20:43:19
看到戚薇的头皮真的替她揪心,为了美也太拼、太下狠功夫了!

看到戚薇的头皮真的替她揪心,为了美也太拼、太下狠功夫了!

乡野小珥
2026-04-27 17:32:04
震惊!大学教师分享女儿留学与欧洲旅行见闻被举报!网友:活该吧

震惊!大学教师分享女儿留学与欧洲旅行见闻被举报!网友:活该吧

火山詩话
2026-04-24 09:20:07
TP-Link把路由器做成平装书大小,谁在买单?

TP-Link把路由器做成平装书大小,谁在买单?

固件更新中
2026-04-28 00:25:10
大变样!捷达汽车全新LOGO正式亮相 网友:像衬衫领子

大变样!捷达汽车全新LOGO正式亮相 网友:像衬衫领子

快科技
2026-04-26 19:29:17
反打9-3!希金斯逆袭奥沙利文进世锦赛八强,巫师终结三连败!

反打9-3!希金斯逆袭奥沙利文进世锦赛八强,巫师终结三连败!

世界体坛观察家
2026-04-27 23:10:02
10亿违建豪宅一夜推平,背后“大人物”被扒,官媒:一点都不冤!

10亿违建豪宅一夜推平,背后“大人物”被扒,官媒:一点都不冤!

网络易不易
2026-04-19 06:05:07
这五个号码千万不要接,一旦接听,银行卡里的钱都可能秒没

这五个号码千万不要接,一旦接听,银行卡里的钱都可能秒没

笑熬浆糊111
2026-04-20 00:05:15
38集扫黑剧,李乃文万茜领衔主演,央视播出,集结十位金牌老戏骨

38集扫黑剧,李乃文万茜领衔主演,央视播出,集结十位金牌老戏骨

草莓解说体育
2026-04-27 17:25:04
美光科技、闪迪、西部数据、希捷科技等美股存储股,盘前均涨约2%

美光科技、闪迪、西部数据、希捷科技等美股存储股,盘前均涨约2%

每日经济新闻
2026-04-27 18:35:09
这个被低估的水果,凭什么叫代谢超级食物?

这个被低估的水果,凭什么叫代谢超级食物?

心事寄山海
2026-04-24 08:58:36
山西泽州发生重大刑案,警方最高悬赏5万元缉凶,为何不公布全名?律师:或为侦查保密,实践中无统一标准

山西泽州发生重大刑案,警方最高悬赏5万元缉凶,为何不公布全名?律师:或为侦查保密,实践中无统一标准

极目新闻
2026-04-27 23:22:34
33岁章泽天风格大变!穿艳俗纱裙、副乳突出,比实际年龄成熟10岁

33岁章泽天风格大变!穿艳俗纱裙、副乳突出,比实际年龄成熟10岁

阿讯说天下
2026-04-18 14:53:39
2026-04-28 02:03:00
开源中国 incentive-icons
开源中国
每天为开发者推送最新技术资讯
7705文章数 34536关注度
往期回顾 全部

科技要闻

DeepSeek V4上线三天,第一批实测出来了

头条要闻

坐在特朗普身边亲历枪击案的女记者 身份非常不一般

头条要闻

坐在特朗普身边亲历枪击案的女记者 身份非常不一般

体育要闻

人类马拉松"破二"新纪元,一场跑鞋军备竞赛

娱乐要闻

黄杨钿甜为“耳环风波”出镜道歉:谣言已澄清

财经要闻

Meta 140亿收购Manus遭中国发改委否决

汽车要闻

不那么小众也可以 smart的路会越走越宽

态度原创

游戏
亲子
本地
数码
公开课

LPL又一超级强队诞生!S赛冠军复出豪取六连胜,小局12-0一场不败

亲子要闻

听劝,五一带孩子出门前,这件事一定要做

本地新闻

云游中国|逛世界风筝都 留学生探秘中国传统文化

数码要闻

6K/3K双模切换!三星这款显示器什么水平?

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版