从 "async trait 不能用" 到原生支持
很长一段时间里,Rust 的 trait 里不能写 async fn,社区只能靠 async-trait 这个宏,把方法脱糖成返回 Pin<Box<dyn Future + Send + '_>> 的形式。它 work,但代价是每次调用一次堆分配、类型信息丢失、错误信息难读。
随着 RPITIT(Return Position Impl Trait In Trait)与 async fn in trait 在新版本稳定,配合 2024 edition 的改进,我们现在可以写出零成本抽象的异步 trait。
基础写法:原生 async trait
use std::future::Future;
trait Repository {
type Error;
// 原生 async fn,编译器自动处理生命周期与 Future 类型
async fn find(&self, id: u64) -> Result<Option<User>, Self::Error>;
}
struct PgRepo { pool: sqlx::PgPool }
impl Repository for PgRepo {
type Error = sqlx::Error;
async fn find(&self, id: u64) -> Result<Option<User>, Self::Error> {
sqlx::query_as!(
User,
r#"select id, name from users where id = $1"#,
id as i64
)
.fetch_optional(&self.pool)
.await
}
}关键细节:Send 边界与 dyn 兼容
原生 async fn 生成的 Future 默认不保证 Send。如果你的 trait 对象要跨线程 spawn,需要显式声明:
trait Service {
// 保证返回的 Future 可跨线程发送
fn handle(&self, req: Request) -> impl Future<Output = Response> + Send;
}
// 使用处
let svc: Box<dyn Service> = todo!();
tokio::spawn(async move { /* 若不加 Send 这里会编译失败 */ });需要 dyn 分发的场景,推荐保留一个显式的 boxed 方法:
trait Fetcher {
fn fetch(&self, url: &str) -> impl Future<Output = String> + Send;
// 为 dyn 场景提供对象安全的包装
fn fetch_boxed<'a>(&'a self, url: &'a str)
-> std::pin::Pin<Box<dyn Future<Output = String> + Send + 'a>>
{
Box::pin(self.fetch(url))
}
}实战:带超时与取消的并发抓取
use tokio::time::{timeout, Duration};
use futures::future::try_join_all;
async fn fetch_all(repo: &impl Repository, ids: Vec<u64>)
-> anyhow::Result<Vec<Option<User>>>
{
let tasks = ids.into_iter().map(|id| async move {
// 超时 + 取消:drop future 即取消,无需额外 API
timeout(Duration::from_millis(500), repo.find(id)).await
});
let results = try_join_all(tasks).await?;
Ok(results.into_iter().collect::<Result<Vec<_>, _>>()?)
}Rust 的取消是“结构化”的:future 被 drop 即取消,配合 RAII 能自动释放连接与锁,这一点比回调式异步安全得多。
2024 edition 带来的顺手改进
use<...>精确捕获:RPIT 现在默认捕获所有生命周期参数,可用impl Future + use<>精确控制,减少意外的借用冲突。impl Trait类型别名:
trait DbFuture<'a, T>: Future<Output = T> + Send + 'a {}
impl<'a, T, F> DbFuture<'a, T> for F where F: Future<Output = T> + Send + 'a {}- 异步闭包:
async || {}稳定后,闭包内可直接.await,省掉一层async moveblock。
性能与调试建议
- 避免无脑
Box::pin:只在真正需要dyn时用,热路径上会引入额外分配。 - 注意 Future 体积:深层嵌套的 async 状态机会撑爆栈,可用
Box::pin分段或#[inline]提示优化。 - 用
tokio-console观察任务:比 println 更靠谱,能看到任务轮询时长与阻塞点。
小结
原生 async trait 让 Rust 异步代码回归“所见即所得”:没有宏魔法、没有堆分配噪音、错误信息可读。配合 2024 edition 的生命周期捕获改进,现在是把老项目里 #[async_trait] 迁出去的好时机。
评论 0
还没有评论,来说两句吧。