2023 年稳定 Trait 中的 async fn

2023 年 5 月 3 日 · Niko Matsakis 和 Tyler Mandry 代表 Rust Async 工作组

异步工作组 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 版本中使用。在我们的上一篇博文中对此进行了详细介绍。

Playground

MVP 第 2 部分:Send bound 和关联返回类型

在 Trait 中使用异步函数时会出现一个同步函数不会出现的问题。许多异步运行时,特别是包括 Tokioasync-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 变得冗长,并且将来可能会考虑更简洁的语法(见下文)。

Playground

MVP 第 3 部分:“Trait 中的 impl trait”(返回位置)

在 Rust 中,异步函数是返回 impl Future 的函数的“语法糖”,Trait 中的异步函数也不例外。作为 MVP 的一部分,我们计划稳定在 Trait 和 Trait impl 中使用 -> impl Trait 表示法。

Trait 中的 Impl trait 有各种用途,但异步编程的一个常见用途是避免捕获所有函数参数,方法是执行一些同步工作,然后返回其余部分的 future。例如,此 LaunchService Trait 声明了一个不捕获 selflaunch 函数(类似于现有的 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 的重要组成部分。

Playground

评估 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、简单的异步迭代器或跨运行时的可移植性。

  1. 如果 RFC 1733 被稳定化,这将更容易。