异步工作组 2023 年的主要目标是稳定 Trait 中 async 函数的“最小可行产品”(MVP)版本。我们目前的目标是 Rust 1.74 版本进行稳定。这篇文章阐述了我们计划发布的功能以及每个功能的状态。
在 11 月,我们发表了关于在 Trait 中对 async fn 进行 nightly 支持的博客,并确定了一些关键的后续步骤,其中最重要的是支持 send bound 以允许在泛型函数中生成。此后,我们进行了大量的设计探索,并收集了一组案例研究,评估当前代码在实践中的效果。
截至目前,这篇博文中描述的所有功能都可以在 nightly 编译器上使用。其中一些工作是使用实验性功能门实现的,以便我们可以进行案例研究并证明其可行性;我们现在正在为这些功能编写 RFC(完整详细信息如下)。
MVP 第 1 部分:对“Trait 中的 async 函数”的核心支持
解释我们即将稳定化的最简单方法是使用代码示例。首先,我们将允许在 Trait 定义中使用 async fn
...
trait HealthCheck {
async fn check(&mut self) -> bool;
}
...然后您可以在相应的 impl 中使用 async fn
impl HealthCheck for MyHealthChecker {
async fn check(&mut self) -> bool {
do_async_op().await
}
}
然后可以像平常一样使用具有异步函数的 Trait
async fn do_health_check(hc: impl HealthCheck) {
if !hc.check().await {
log_health_check_failure().await;
}
}
状态: 此功能在 RFC 3185 中进行了描述,该 RFC 于 2021 年 12 月 7 日合并,并且可在 nightly 版本中使用。在我们的上一篇博文中对此进行了详细介绍。
MVP 第 2 部分:Send bound 和关联返回类型
在 Trait 中使用异步函数时会出现一个同步函数不会出现的问题。许多异步运行时,特别是包括 Tokio 和 async-std 的默认配置,都使用工作窃取线程调度器。这意味着 future 可能会在工作线程之间动态移动以实现负载均衡。因此,future 必须仅捕获 Send
数据。
但是,如果您编写一个在这些运行时之一上生成任务的泛型异步函数,您将开始收到编译错误(playground)
async fn do_health_check_par(hc: impl HealthCheck) {
tokio::task::spawn(async move {
if !hc.check().await {
log_health_check_failure().await;
}
});
}
问题在于 hc.check()
返回的 future 不能保证是 Send
。它可能会访问非 Send 数据。解决方案是添加一个 Send
bound,但鉴于这是一个异步函数,如何做到这一点并不明显。我们如何谈论调用 hc.check()
返回的 future?关联返回类型提供了答案。我们可以转换上面的函数以使用显式类型参数 HC
(而不是 impl HealthCheck
),然后添加一个新的 bound HC::check(): Send
。这表示“HC::check
返回的值必须是 Send
类型”
async fn do_health_check_par<HC>(hc: HC)
where
HC: HealthCheck + Send + 'static,
HC::check(): Send, // <-- associated return type
{
tokio::task::spawn(async move {
if !hc.check().await {
log_health_check_failure().await;
}
});
}
当然,为了使用此表示法,我们必须从采用 impl HealthCheck
重写为显式 HC
类型参数,这有点不幸。RFC 2289“关联类型边界”引入了一种紧凑的表示法来解决此问题。该 RFC 不是此 MVP 的一部分,但如果它被稳定化,那么可以简单地写成
async fn do_health_check_par(hc: impl HealthCheck<check(): Send> + Send + 'static) {
// -------------
tokio::task::spawn(async move {
if !hc.check().await {
log_health_check_failure().await;
}
});
}
在我们的上一篇文章中,我们假设这个问题在实践中可能不会经常发生。但是,我们的案例研究发现它非常频繁地出现,因此我们认为需要一个解决方案。我们探索了多种解决方案,并得出结论,关联返回类型 (ART) 是一种灵活且相当符合人体工程学的构建块,这使它们非常适合 MVP。
状态: 关联返回类型有一个实验性实现,我们目前正在起草 RFC。有几个需要修复的未解决的错误。我们还发现,在具有多个方法的 Trait 中,ART 变得冗长,并且将来可能会考虑更简洁的语法(见下文)。
MVP 第 3 部分:“Trait 中的 impl trait”(返回位置)
在 Rust 中,异步函数是返回 impl Future
的函数的“语法糖”,Trait 中的异步函数也不例外。作为 MVP 的一部分,我们计划稳定在 Trait 和 Trait impl 中使用 -> impl Trait
表示法。
Trait 中的 Impl trait 有各种用途,但异步编程的一个常见用途是避免捕获所有函数参数,方法是执行一些同步工作,然后返回其余部分的 future。例如,此 LaunchService
Trait 声明了一个不捕获 self
的 launch
函数(类似于现有的 Tower Service
Trait)
trait LaunchService {
fn launch(
&mut self,
request: Request,
) -> impl Future<Output = u32>;
// -------------------------
// Does not capture `self` as it does
// not include a `+ '_`.
}
由于 async fn
是返回 impl Future
的常规函数的语法糖,因此这两种语法形式可以互换使用。
trait HealthCheck {
async fn check(&mut self) -> bool;
}
impl HealthCheck for MyType {
fn check(&mut self) -> impl Future<Output = bool> + '_ { ... }
}
尽管在异步中经常出现对“Trait 中的 impl trait”的需求,但它们是一项通用功能,将在许多与异步无关的上下文中使用(例如,从 Trait 方法返回迭代器)。
状态: Trait 中返回位置 impl trait 有一个实验性实现,并在 RFC 3425 中进行了描述,该 RFC 目前处于开放状态。此功能可以独立存在,但它是 Trait 中 async fn
的重要组成部分。
评估 MVP
为了评估此 MVP 的实用性,工作组收集了五个案例研究,涵盖了 AWS SDK 中使用的构建器-提供程序模式;在 tower 中使用异步函数的潜在用途以及在 embassy 中的实际使用情况、Fuchsia 网络堆栈和Microsoft 内部工具。这些研究验证了上述功能足以在 Trait 中使用异步函数来完成各种任务,尽管某些情况需要解决方法(因此称为“MVP”)。
MVP 将不支持或无法很好支持的功能
案例研究揭示了 MVP 不能很好地支持两种情况,但是它们都有可用的解决方法。这些解决方法是机械的,一旦 MVP 在稳定版上可用,就可以通过自定义派生或其他 crates.io 上的 crate 来自动执行它们。
建模动态分发
在 MVP 中,使用异步函数的 Trait 不是“dyn 安全”的,这意味着它们不支持动态分发。例如,给定我们之前看到的 HealthCheck
Trait,不能写 Box
。
起初,这似乎是一个至关重要的限制,因为许多用例都需要动态分发!但是事实证明有一个解决方法。可以在您的 crate 内部定义一个“擦除”的 Trait,该 Trait 启用动态分发。此过程由 erased serde 等 crate 开创,并在 构建器-提供程序案例研究中进行了详细说明。
为了在短期内使此解决方法更容易,我们计划提供一个 proc 宏来自动执行它。将来,async fn应该可以直接与 dyn Trait
一起使用。
Send bound 很冗长,对于具有大量方法的 Trait 尤其如此
关联返回类型提案对于具有单个方法的 Trait 效果很好,但对于具有多个方法的 Trait 来说可能会很烦人。一个方便的解决方案是使用“Trait 别名模式”:1
trait SendHealthCheck
where
Self: HealthCheck + Send,
Self::check(): Send,
{}
impl<T> SendHealthCheck for T
where
T: HealthCheck + Send,
T::check(): Send,
{}
使用这样的模式意味着您可以编写 T: SendHealthCheck
。我们计划提供一个 proc 宏来为您编写这些 Trait 别名,因此您可以编写更像这样的代码
#[make_alias(Send = "SendHealthCheck")]
trait HealthCheck {
async fn check(&mut self) -> bool;
}
将来,类似Trait 转换器的东西可以提供更简洁的语法,而无需 proc 宏。但是由于存在需要关联返回类型提供的细粒度控制的用例,因此我们选择首先稳定它们,并在获得经验后考虑更简洁的语法。
时间表和路线图
我们的目标是在 Rust 1.74 中稳定 MVP,该版本将于 2023-11-16 发布。此功能的分支窗口于 7 月 14 日打开,于 8 月 24 日关闭。要实际在 1.74 中稳定下来,我们希望为发布分支剪切之前可能出现的错误修复留出空间。此目标的关键里程碑如下
- [x] MVP 实现
- [x] 案例研究评估完成
- [ ] 接受返回位置 impl trait 的 RFC(目标:2023-05-31)
- [ ] 接受关联返回类型的 RFC(目标:2023-06-15)
- [ ] 评估期和错误修复(目标:2023-06-30)
- [ ] 撰写稳定化报告(目标:2023-07-01)
- [ ] 1.74.0 版本的稳定化完成(目标:2023-07-21)
你可以在我们的 github 项目中找到完整的 时间线。
接下来是什么?
那么,一旦这个 MVP 完成,接下来是什么?我们接下来的直接目标是在 2024 年发布对 动态分发 和 异步闭包 的支持。这将共同为解决未来的异步问题奠定坚实的基础,例如支持异步 drop、简单的异步迭代器或跨运行时的可移植性。