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

如何在同步的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-10-09 14:09:32
日本B联赛发展太迅猛!一支球队年赚9700万,目标培养5名NBA球员

日本B联赛发展太迅猛!一支球队年赚9700万,目标培养5名NBA球员

林小湜体育频道
2026-10-09 14:45:14
山姆拟收紧亲友卡规则:同一用户每365天最多绑定2张亲友卡

山姆拟收紧亲友卡规则:同一用户每365天最多绑定2张亲友卡

时代财经
2026-10-09 17:10:13
江苏省卫健委副主任鲍军、江苏省知识产权局局长李杰,拟任省级机关正职

江苏省卫健委副主任鲍军、江苏省知识产权局局长李杰,拟任省级机关正职

小影的娱乐
2026-10-09 10:46:49
月薪可达六位数!小伙拜师学手艺,还没毕业就拿证,钉蹄师全国仅70余人

月薪可达六位数!小伙拜师学手艺,还没毕业就拿证,钉蹄师全国仅70余人

环球网资讯
2026-10-09 08:21:31
无底线了!内塔尼亚胡之子被伊朗特工暗杀,弃行李连夜逃回以色列

无底线了!内塔尼亚胡之子被伊朗特工暗杀,弃行李连夜逃回以色列

梦史
2026-08-29 06:21:20
梁文锋的500亿,投向一座北方小城

梁文锋的500亿,投向一座北方小城

华尔街见闻官方
2026-10-08 19:42:15
天冷了,每天来一杯,悄悄给肝脏洗个澡,痰湿消了,觉也睡得香了

天冷了,每天来一杯,悄悄给肝脏洗个澡,痰湿消了,觉也睡得香了

宝哥精彩赛事
2026-10-08 11:41:25
事态已经全面升级!赖清德有可能成为新中国历史上,唯一一位在任台湾地区领导人中出现重大变故的人物

事态已经全面升级!赖清德有可能成为新中国历史上,唯一一位在任台湾地区领导人中出现重大变故的人物

人生录
2026-09-11 00:05:21
上海一学校饲养梅花鹿被指吃不饱、浑身是伤,多方回应:它吃得比我们都好,近期将送走

上海一学校饲养梅花鹿被指吃不饱、浑身是伤,多方回应:它吃得比我们都好,近期将送走

潇湘晨报
2026-10-09 17:19:12
高芙还想收中国球迷礼物:我额外准备了一个行李袋!被全网群嘲

高芙还想收中国球迷礼物:我额外准备了一个行李袋!被全网群嘲

章蠞户外
2026-10-09 11:34:36
房价很大可能重走1998年老路!所有人一定要提前做好心理准备

房价很大可能重走1998年老路!所有人一定要提前做好心理准备

偷喝一口奶
2026-09-11 08:36:30
美国30年为何没对中国下死手?奥巴马有一句话,他说:美国不是不想对中国下死手,是中国人根本没给机会。

美国30年为何没对中国下死手?奥巴马有一句话,他说:美国不是不想对中国下死手,是中国人根本没给机会。

回京历史梦
2026-10-06 16:51:33
15999元起!苹果首款折叠屏iPhone Duo下周预购:京东预约人数超198万

15999元起!苹果首款折叠屏iPhone Duo下周预购:京东预约人数超198万

快科技
2026-10-09 10:36:33
网友称有人将小广告贴到“光荣之家”标识牌上,相关部门:不能污损光荣牌,将联合公安部门调查

网友称有人将小广告贴到“光荣之家”标识牌上,相关部门:不能污损光荣牌,将联合公安部门调查

极目新闻
2026-10-08 18:10:12
济公游本昌去世15天,女儿游思涵官宣消息,一个举动让全网泪目

济公游本昌去世15天,女儿游思涵官宣消息,一个举动让全网泪目

林雁飞
2026-10-09 17:18:06
6场5负!世界杯两大黑马面临降级和出局,意大利葡萄牙渐入佳境

6场5负!世界杯两大黑马面临降级和出局,意大利葡萄牙渐入佳境

嗨皮看球
2026-10-08 18:39:54
0-3爆冷背后:张本美和亲手终结何卓佳九年陪练时代

0-3爆冷背后:张本美和亲手终结何卓佳九年陪练时代

慢歌轻步谣
2026-10-09 00:32:54
婆婆又叫小叔子一家来过年,我没有吵闹,除夕当天她再也笑不出来

婆婆又叫小叔子一家来过年,我没有吵闹,除夕当天她再也笑不出来

风起见你
2026-10-09 15:43:45
未来一周,喜鹊来报喜,三大生肖正财稳健,偏财也有小惊喜!

未来一周,喜鹊来报喜,三大生肖正财稳健,偏财也有小惊喜!

毅谈生肖
2026-10-09 11:51:12
2026-10-09 18:12:49
开源中国 incentive-icons
开源中国
每天为开发者推送最新技术资讯
7840文章数 34557关注度
往期回顾 全部

科技要闻

字节"干扰模型训练"实习生融资近2亿元

头条要闻

最淡泊的福建首富静静地走了 其家族现坐拥105亿财富

头条要闻

最淡泊的福建首富静静地走了 其家族现坐拥105亿财富

体育要闻

30天30队·鹈鹕:胖虎又一年“如果”

娱乐要闻

吴奇隆宣布重要决定,走上谢霆锋老路

财经要闻

央行为何发布关于人民币汇率的政策立场

汽车要闻

比亚迪第三代唐内饰发布 两种风格/巨幕屏最抢眼

态度原创

旅游
艺术
家居
教育
时尚

旅游要闻

七天客流超31.4万人次,第二届捕风生活节聚起横琴烟火气

艺术要闻

当代油画家区础坚 女性人物油画选

家居要闻

2026建博会(广州) 公装联探展交流活动

教育要闻

刚刚,北京市教委发布——

从夜幕金辉到另类宇宙,巴黎更美了吗?

无障碍浏览 进入关怀版