Rust同步上下文调用异步HTTP RPC代码阻塞问题求助
我需要在Rust的同步上下文中调用HTTP RPC接口,但无法修改当前上下文的调用方式为异步。尝试用executor::block_on(get_snowflake_id())调用异步函数get_snowflake_id时,代码陷入永久阻塞。之后又尝试了以下实现:
pub fn get_uniq_id() -> Option<i64> { task::block_in_place(|| { tokio::runtime::Handle::current().block_on(get_snowflake_id()) }) }
但出现错误提示can call blocking only when running on the multi-threaded runtime。我的应用入口为:
#[actix_web::main] async fn main() -> std::io::Result<()> {}
get_snowflake_id函数的具体实现如下:
use std::env; use log::error; use reqwest::Client; use rust_wheel::model::response::api_response::ApiResponse; pub async fn get_snowflake_id() -> Option<i64> { let client = Client::new(); let infra_url = env::var("INFRA_URL").expect("INFRA_URL must be set"); let url = format!("{}{}", infra_url, "/infra-inner/util/uniqid/gen"); let resp = client .get(format!("{}", url)) .body("{}") .send() .await; if let Err(e) = resp { error!("get id failed: {}", e); return None; } let text_response = resp.unwrap().text().await; if let Err(e) = text_response { error!("extract text failed: {}", e); return None; } let resp_str = text_response.unwrap_or_default(); let resp_result = serde_json::from_str::<ApiResponse<i64>>(&resp_str); if let Err(pe) = resp_result { error!("parse failed: {}, response: {}", pe, &resp_str); return None; } Some(resp_result.unwrap().result) }
请问该如何正确在同步上下文中调用这段异步代码?
核心原因
问题本质是Actix Web默认使用单线程Runtime,而block_in_place和Handle::current().block_on依赖多线程Runtime才能安全执行阻塞操作;同时直接在异步上下文嵌套block_on会导致线程死锁——Runtime的工作线程被阻塞后,无法调度后续异步任务完成。
可行实现方案
方案1:创建独立多线程Runtime
在同步函数内部创建独立的Tokio多线程Runtime,完全隔离Actix的Runtime,避免冲突:
pub fn get_uniq_id() -> Option<i64> { // 初始化多线程Runtime let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); rt.block_on(get_snowflake_id()) }
优点:无需修改现有Actix配置,逻辑独立;缺点:每次调用都会创建Runtime,可通过全局缓存优化。
方案2:全局缓存Runtime实例
用全局懒加载的方式缓存Runtime,避免重复初始化的性能损耗:
use once_cell::sync::Lazy; // 全局缓存多线程Runtime static RUNTIME: Lazy<tokio::runtime::Runtime> = Lazy::new(|| { tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime") }); pub fn get_uniq_id() -> Option<i64> { RUNTIME.block_on(get_snowflake_id()) }
需要在Cargo.toml中添加依赖:
once_cell = "1.18.0"
方案3:切换Actix到多线程Runtime
修改应用入口,强制使用多线程Runtime,这样原有的block_in_place写法即可正常工作:
#[actix_web::main] async fn main() -> std::io::Result<()> { // 构建多线程Runtime并替换默认配置 let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(num_cpus::get()) .enable_all() .build() .unwrap(); rt.block_on(async { // 原有main逻辑移至此处 Ok(()) }) }
同步函数修改为:
pub fn get_uniq_id() -> Option<i64> { tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(get_snowflake_id()) }) }
注意:此方案需确保整个应用都基于多线程Runtime运行,避免单线程场景下的死锁风险。
额外优化建议
reqwest::Client是线程安全且可重用的,可提前全局初始化,避免每次调用get_snowflake_id都创建新实例:
use once_cell::sync::Lazy; static HTTP_CLIENT: Lazy<reqwest::Client> = Lazy::new(reqwest::Client::new); pub async fn get_snowflake_id() -> Option<i64> { let client = &HTTP_CLIENT; // 后续业务逻辑保持不变 }
内容的提问来源于stack exchange,提问作者Dolphin

