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

如何在同步的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.

相关推荐
热点推荐
下一个哈兰德!曼联锁定 7300 万新神锋!21 岁天才完美复刻

下一个哈兰德!曼联锁定 7300 万新神锋!21 岁天才完美复刻

澜归序
2026-08-10 06:24:26
温岭9岁男孩被卷走后续,救援队曝一个细节,大家心里都有谱了

温岭9岁男孩被卷走后续,救援队曝一个细节,大家心里都有谱了

奇思妙想草叶君
2026-08-09 19:13:39
9.5亿用户也救不了Gemini

9.5亿用户也救不了Gemini

影子聊科技
2026-08-09 19:36:20
陪玩陪睡只是皮毛!继手伸进裤子后,又一女星自曝,50多都不放过

陪玩陪睡只是皮毛!继手伸进裤子后,又一女星自曝,50多都不放过

不似少年游
2026-06-22 19:32:51
45了还在拍三级片,败光两任的亿万家产,如今坦言后悔嫁给钟镇涛

45了还在拍三级片,败光两任的亿万家产,如今坦言后悔嫁给钟镇涛

寒士之言本尊
2026-07-28 14:52:44
国内一航班爆发恶性斗殴冲突!飞机正处爬升期,两乘客大打出手!

国内一航班爆发恶性斗殴冲突!飞机正处爬升期,两乘客大打出手!

追踪之点
2026-08-08 22:33:55
全球独一份的巨婴外交!印度一边反华排华,一边找中国要超国民待遇

全球独一份的巨婴外交!印度一边反华排华,一边找中国要超国民待遇

浪子的烟火人间
2026-08-10 08:55:06
台风“琵鹭”生成!汕头38.7℃!已出现8级大风!未来将持续高温!

台风“琵鹭”生成!汕头38.7℃!已出现8级大风!未来将持续高温!

ilove汕头
2026-08-09 17:29:49
历史轮回再现?下半年存款利率复刻 2016 年走势,存钱的人早看早省心

历史轮回再现?下半年存款利率复刻 2016 年走势,存钱的人早看早省心

牛锅巴小钒
2026-08-10 00:23:30
中央电视台直播录播8月10日至16日乒乓球比赛

中央电视台直播录播8月10日至16日乒乓球比赛

乒乓球球
2026-08-10 00:35:08
1978年,邱清泉之子邱国渭以蚂蚁搬家的方式,偷偷将全家人送往美国,当时政府已经发现他的意图,为何没有阻拦?

1978年,邱清泉之子邱国渭以蚂蚁搬家的方式,偷偷将全家人送往美国,当时政府已经发现他的意图,为何没有阻拦?

人生录
2026-07-24 00:05:08
孙梅君最新署名文章

孙梅君最新署名文章

政知新媒体
2026-08-10 09:27:48
金球奖官方:2022年起金球奖评选标准改革:个人表现优先于集体荣誉

金球奖官方:2022年起金球奖评选标准改革:个人表现优先于集体荣誉

懂球帝
2026-08-09 19:44:16
封神爆发!巴萨再爆绝世妖星!19 岁天才一战征服弗里克

封神爆发!巴萨再爆绝世妖星!19 岁天才一战征服弗里克

奶盖熊本熊
2026-08-10 05:24:46
丈夫年薪快700万,我带娃8年没工作,他提离婚我没犹豫,他说别墅豪车都给我,一个月后他回家拿东西,推开门愣住了

丈夫年薪快700万,我带娃8年没工作,他提离婚我没犹豫,他说别墅豪车都给我,一个月后他回家拿东西,推开门愣住了

墨染尘香
2026-08-08 15:13:25
1948 年黄百韬部被围,五位军长死守不降,激战方知非嫡系最顽强

1948 年黄百韬部被围,五位军长死守不降,激战方知非嫡系最顽强

磊子讲史
2026-06-26 13:53:56
最荒唐的双标!自己锁死我们,还要我们优待印度人、管控国人情绪?

最荒唐的双标!自己锁死我们,还要我们优待印度人、管控国人情绪?

浪子的烟火人间
2026-08-10 08:53:13
20多年前,村里寡妇说我糟蹋了她,我被打断腿赶出村,15年后我开着豪车回乡祭祖,她拉着一个少年跪在我车前说:这是你儿子

20多年前,村里寡妇说我糟蹋了她,我被打断腿赶出村,15年后我开着豪车回乡祭祖,她拉着一个少年跪在我车前说:这是你儿子

墨染尘香
2026-08-08 09:10:02
一觉醒来,“白海豚”到哪儿了?闵行发布暴雨橙色预警!

一觉醒来,“白海豚”到哪儿了?闵行发布暴雨橙色预警!

新浪财经
2026-08-10 08:02:25
两任妻子郁郁而终,6子女恨之入骨,独活93岁原配子女无一送终

两任妻子郁郁而终,6子女恨之入骨,独活93岁原配子女无一送终

明月清风阁
2026-08-10 07:25:41
2026-08-10 09:44:49
开源中国 incentive-icons
开源中国
每天为开发者推送最新技术资讯
7840文章数 34558关注度
往期回顾 全部

科技要闻

马斯克又放大招:星链要吃掉全球一半流量

头条要闻

伊朗最高领袖与总统会谈 或为回应"穆杰塔巴病危"报道

头条要闻

伊朗最高领袖与总统会谈 或为回应"穆杰塔巴病危"报道

体育要闻

豪尔赫·梅西去世,球王的人生导师离开了

娱乐要闻

贾冰的一场饭局,给娱乐圈上了一课

财经要闻

宇树今申购 具身智能赛道将迎“估值锚”

汽车要闻

增配激光雷达 全新一代smart精灵1号限时权益价14.99万起

态度原创

房产
时尚
家居
公开课
军事航空

房产要闻

一手信息流出!仁恒雲启西岸开盘,爆卖110套!

入秋一定要拥有的衣服:针织背心,太好看了

家居要闻

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

公开课

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

军事要闻

伊朗称重开霍尔木兹海峡前提是美国满足5个条件

无障碍浏览 进入关怀版