You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何避免为适配一个依赖项而将所有Rust代码改为异步?

如何避免为适配一个依赖项而将所有Rust代码改为异步?

我太懂这种两难处境了!之前你为了图省心,用reqwest的阻塞特性写了libA,封装了一堆REST API调用的同步函数,用起来完全是常规Rust代码的思路,现有多个二进制项目用着都好好的。结果新搞的异步二进制一集成就报错,抛出的错误是:

Cannot drop a runtime in a context where blocking is not allowed. This happens when a runtime is dropped from within an asynchronous context.

原因你也猜中了——就是嵌套运行时在搞鬼:你的异步二进制已经启动了一个异步运行时(比如Tokio),而libA里的reqwest阻塞客户端又会偷偷创建自己的运行时,两个嵌套就触发了这个错误。

其实不用急着把整个libA改成异步,这里有几个更灵活的方案:

方案一:用block_in_place隔离阻塞调用

如果新的异步项目用的是Tokio运行时,可以用tokio::task::block_in_place把libA的同步调用包起来。这个函数能把阻塞任务放到专门的线程池里,避免阻塞异步任务的工作线程,也能绕开嵌套运行时的问题。

示例代码大概是这样:

async fn call_libA() -> Result<(), Box<dyn std::error::Error>> {
    tokio::task::block_in_place(|| {
        libA::some_api_call()
    })?;
    Ok(())
}

不过要注意两点:

  • 这个方法只适用于允许阻塞的Tokio运行时配置(默认模式是允许的,但如果你的运行时是current_thread模式就不行)
  • 虽然能解决问题,但本质还是在异步代码里套同步阻塞,性能上不如原生异步调用,适合临时过渡或者调用频率不高的场景

方案二:让libA同时支持同步和异步两种模式

这是更长期的优雅方案,把libA改造成同时提供同步和异步API,让不同的调用方按需选择。

具体可以这么做:

  • 先把核心的API逻辑抽离成不依赖同步/异步的通用部分,比如请求参数构造、响应解析这些
  • 然后分别基于reqwest的阻塞客户端和异步客户端封装同步、异步的API函数
  • 用Rust的特性(features)来控制编译,比如默认编译同步API,用户可以通过启用async特性来引入异步API

举个简化的例子:
首先在libA的Cargo.toml里加特性配置:

[features]
default = ["blocking"]
blocking = ["reqwest/blocking"]
async = ["reqwest"]

然后代码里分模块实现:

// 通用逻辑模块
mod common {
    use std::collections::HashMap;

    pub fn build_request_params() -> HashMap<String, String> {
        // 构造请求参数的通用代码
        let mut params = HashMap::new();
        params.insert("key".to_string(), "value".to_string());
        params
    }

    pub fn parse_response(response: reqwest::Response) -> Result<MyResponse, MyError> {
        // 解析响应的通用代码
        let data = response.json().map_err(|e| MyError::ParseError(e.to_string()))?;
        Ok(data)
    }

    #[derive(Debug, serde::Deserialize)]
    pub struct MyResponse {
        // 响应结构体定义
        pub id: u32,
        pub content: String,
    }

    #[derive(Debug)]
    pub enum MyError {
        ReqwestError(String),
        ParseError(String),
    }

    impl From<reqwest::Error> for MyError {
        fn from(e: reqwest::Error) -> Self {
            MyError::ReqwestError(e.to_string())
        }
    }
}

// 同步API模块,默认编译
#[cfg(feature = "blocking")]
pub mod blocking {
    use super::common;
    use reqwest::blocking::Client;

    pub fn some_api_call() -> Result<common::MyResponse, common::MyError> {
        let client = Client::new();
        let params = common::build_request_params();
        let response = client.get("https://api.example.com/endpoint")
            .query(&params)
            .send()?;
        common::parse_response(response.error_for_status()?)
    }
}

// 异步API模块,启用async特性时编译
#[cfg(feature = "async")]
pub mod r#async {
    use super::common;
    use reqwest::Client;

    pub async fn some_api_call() -> Result<common::MyResponse, common::MyError> {
        let client = Client::new();
        let params = common::build_request_params();
        let response = client.get("https://api.example.com/endpoint")
            .query(&params)
            .send()
            .await?;
        common::parse_response(response.error_for_status()?)
    }
}

这样一来,原来的同步二进制继续用libA::blocking::some_api_call(),新的异步项目启用async特性后用libA::r#async::some_api_call().await,完美兼顾两种场景,也不用重构所有现有代码。

方案三:给libA的阻塞客户端传入外部运行时(进阶)

reqwest的阻塞客户端其实允许你传入一个现有的Tokio运行时,而不是自己创建新的。这样就能避免嵌套运行时的问题,不过这个方法需要修改libA的代码,让它接受外部运行时的参数。

示例大概是:

// 在libA里新增一个接受运行时的API
pub fn some_api_call_with_runtime(rt: &tokio::runtime::Runtime) -> Result<common::MyResponse, common::MyError> {
    rt.block_on(async {
        let client = reqwest::Client::new();
        // 这里用异步客户端的逻辑,然后用运行时阻塞等待结果
        let response = client.get("https://api.example.com/endpoint")
            .send()
            .await?;
        common::parse_response(response.error_for_status()?)
    })
}

然后在异步项目里,把自己的运行时传进去:

async fn call_libA() -> Result<(), Box<dyn std::error::Error>> {
    let rt = tokio::runtime::Handle::current();
    let result = rt.block_on(async {
        libA::some_api_call_with_runtime(rt.runtime())
    })?;
    Ok(())
}

不过这个方法相对复杂,需要调用方管理运行时,不如方案二灵活,适合特定场景下的快速调整。

总的来说,方案二是最可持续的,虽然需要花点时间重构libA,但能一劳永逸解决同步异步兼容的问题;方案一适合临时救急,快速让新的异步项目跑起来。

备注:内容来源于stack exchange,提问作者Michael

火山引擎 最新活动