异步工作组很高兴地宣布,现在可以在 nightly 编译器中的 trait 中使用 async fn
。您现在可以编写如下代码
#![feature(async_fn_in_trait)]
trait Database {
async fn fetch_data(&self) -> String;
}
impl Database for MyDb {
async fn fetch_data(&self) -> String { ... }
}
一个完整的可运行示例可以在 playground 中找到。我们会介绍一些限制,以及一些待解决的已知 bug,但我们认为它已经准备好供一些用户尝试了。请继续阅读了解具体信息。
回顾:async/await 在 Rust 中如何工作
async
和 .await
是 Rust 中编写异步代码的重大改进。在 Rust 中,async fn
返回一个 Future
,它是一些表示正在进行的异步计算的对象。
future 的类型实际上不会出现在 async fn
的签名中。当您编写像这样的异步函数时
async fn fetch_data(db: &MyDb) -> String { ... }
编译器会将其重写为如下形式
fn fetch_data<'a>(db: &'a MyDb) -> impl Future<Output = String> + 'a {
async move { ... }
}
这种“脱糖”后的签名是您可以自己编写的,它对于检查底层发生了什么很有用。这里的 impl Future
语法表示一些实现了 Future
的不透明类型。
future 是一个状态机,负责知道下次唤醒时如何继续取得进展。当您在 async
代码块中编写代码时,编译器会为您生成一个特定于该异步代码块的 future 类型。此 future 类型没有名称,因此我们必须在函数签名中使用不透明类型。
async fn
的历史性问题
trait 中 Trait 是 Rust 中抽象的基本机制。那么,如果您想在 trait 中放入一个异步方法会发生什么?每个 async
代码块或函数都会创建一个唯一的类型,因此您会希望表达每个实现都可以为返回类型使用不同的 Future。值得庆幸的是,我们有相关的类型来实现这一点
trait Database {
type FetchData<'a>: Future<Output = String> + 'a where Self: 'a;
fn fetch_data<'a>(&'a self) -> FetchData<'a>;
}
请注意,此关联类型是泛型的。该语言尚未支持泛型关联类型...直到现在!不幸的是,即使有了 GAT,您仍然无法编写使用 async
的 trait *实现*
impl Database for MyDb {
type FetchData<'a> = /* what type goes here??? */;
fn fetch_data<'a>(&'a self) -> FetchData<'a> { async move { ... } }
}
由于您无法命名由 async 代码块构造的类型,因此唯一的选择是使用不透明类型(我们之前看到的 impl Future
)。但这些在关联类型中不受支持!1
稳定编译器中可用的解决方法
因此,要在 trait 中编写 async fn
,我们需要在 impl 中指定一个具体的类型。现在有几种方法可以实现这一点。
运行时类型擦除
首先,我们可以通过使用 dyn
擦除 future 类型来避免编写 future 类型。以我们上面的示例为例,您可以像这样编写您的 trait
trait Database {
fn fetch_data(&self)
-> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}
这要冗长得多,但它实现了将异步与 trait 结合起来的目标。更重要的是,async-trait proc 宏 crate 为您重写了代码,允许您简单地编写
#[async_trait]
trait Database {
async fn fetch_data(&self) -> String;
}
#[async_trait]
impl Database for MyDb {
async fn fetch_data(&self) -> String { ... }
}
对于可以使用它的人来说,这是一个很好的解决方案!
不幸的是,并非所有人都能使用。您不能在 no_std 上下文中使用 Box
。动态分派和分配会带来开销,对于高度性能敏感的代码来说,这种开销可能会难以承受。最后,它在 trait 本身中加入了很多假设:使用 Box
进行分配、动态分派以及 future 的 Send
性。这使得它不适合许多库。
此外,用户期望能够在 trait 中编写 async fn
,添加外部 crate 依赖项的体验是一种阻碍,这使得 Rust 的异步使用起来很困难。
poll
实现
手动 需要在零开销或 no_std 上下文中工作的 trait 还有另一个选择:它们可以从Future
trait中获取轮询的概念,并将其直接构建到它们的接口中。如果 future 完成,则 Future::poll
方法返回 Poll::Ready(Output)
,如果 future 正在等待其他事件,则返回 Poll::Pending
。
例如,您可以在当前不稳定版本的AsyncIterator trait 中看到这种模式。AsyncIterator::poll_next
的签名是 Future::poll
和 Iterator::next
的交叉。
pub trait AsyncIterator {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}
在 async/await 之前,编写手动 poll
实现非常常见。不幸的是,事实证明它们很难正确编写。在我们去年进行的愿景文档流程中,我们收到了许多关于这是如何极其困难以及 Rust 用户的bug 来源的报告。
事实上,编写手动 poll 实现的困难是首先向核心语言添加 async/await 的主要原因。
nightly 中支持的内容
我们一直在努力直接在 trait 中支持 async fn
,并且一个实现最近已登陆 nightly!该功能仍然有一些粗糙的边缘,但让我们看看您可以用它做什么。
首先,正如您可能期望的那样,您可以像上面一样编写和实现 trait。
#![feature(async_fn_in_trait)]
trait Database {
async fn fetch_data(&self) -> String;
}
impl Database for MyDb {
async fn fetch_data(&self) -> String { ... }
}
这将使我们能够做的一件事是标准化我们一直在等待此功能的新 trait。例如,上面的 AsyncIterator
trait 比它的类似物 Iterator
复杂得多。有了新的支持,我们可以简单地这样编写
#![feature(async_fn_in_trait)]
trait AsyncIterator {
type Item;
async fn next(&mut self) -> Option<Self::Item>;
}
标准库中的 trait 很有可能最终会像这样!现在,您也可以使用 async_iterator
crate 中的 trait 并使用它编写通用代码,就像您通常所做的那样。
async fn print_all<I: AsyncIterator>(mut count: I)
where
I::Item: Display,
{
while let Some(x) = count.next().await {
println!("{x}");
}
}
限制:从泛型生成
这里有一个问题!如果您尝试从像 print_all
这样的泛型函数中生成,并且(像大多数异步用户一样)您使用需要生成的任务是 Send
的工作窃取执行程序,您将遇到一个不易解决的错误。2
fn spawn_print_all<I: AsyncIterator + Send + 'static>(mut count: I)
where
I::Item: Display,
{
tokio::spawn(async move {
// ^^^^^^^^^^^^
// ERROR: future cannot be sent between threads safely
while let Some(x) = count.next().await {
// ^^^^^^^^^^^^
// note: future is not `Send` as it awaits another future which is not `Send`
println!("{x}");
}
});
}
您可以看到我们在函数签名中添加了 I: Send
约束,但这还不够。要满足此错误,我们需要说由 next()
返回的 future 是 Send
。但是正如我们在本文开头看到的那样,异步函数返回匿名类型。无法表达这些类型的约束。
我们将在后续文章中探讨此问题的潜在解决方案。但就目前而言,您可以做一些事情来摆脱这种情况。
假设:这种情况并不常见
首先,您可能会惊讶地发现这种情况在实践中并不经常发生。例如,我们可以生成一个调用上述 print_all
函数的任务,没有任何问题
async fn do_something() {
let iter = Countdown::new(10);
executor::spawn(print_all(iter));
}
这在没有任何 Send
约束的情况下都可以工作!之所以可以这样做,是因为 do_something
知道我们迭代器 Countdown
的具体类型。编译器知道此类型是 Send
,因此 print_all(iter)
会生成一个 Send
的 future。3
一种假设是,虽然人们会遇到这个问题,但他们遇到它的频率相对较低,因为大多数时候 spawn
不会在泛型于具有异步函数的 trait 的代码中被调用。
我们想开始收集人们对此的实际经验数据。如果您有相关的经验可以分享,请在此问题上发表评论。
当它发生时
最终您可能会想从泛型于您调用的异步 trait 的上下文中生成。那该怎么办呢!?
现在可以使用另一个新的仅限 nightly 的功能 return_position_impl_trait_in_trait
,在您的 trait 中直接表达您需要的约束
#![feature(return_position_impl_trait_in_trait)]
trait SpawnAsyncIterator: Send + 'static {
type Item;
fn next(&mut self) -> impl Future<Output = Option<Self::Item>> + Send + '_;
}
在这里,我们将 async fn
脱糖为返回 impl Future + '_
的常规函数,它的工作方式与普通的 async fn
相同。它更冗长,但它为我们提供了一个放置 + Send
约束的位置!更重要的是,您可以在此 trait 的 impl
中继续使用 async fn
。
这种方法的缺点是 trait 的通用性降低,使其不太适合库代码。如果您想维护 trait 的两个单独版本,您可以这样做,并且(或许)提供宏来简化两者的实现。
此解决方案旨在作为临时方案。我们希望在语言本身中实现更好的解决方案,但由于这是一个仅限 nightly 的功能,我们希望尽快让更多人尝试它。
限制:动态分派
最后一个限制:您不能使用 dyn Trait
调用 async fn
。支持此功能的设计已经存在4,但仍处于早期阶段。如果您需要从 trait 进行动态分派,那么现在最好使用 async_trait
宏。
稳定化路径
异步工作组希望将有用的东西交给 Rust 用户,即使它不能做他们可能想要的一切。这就是为什么尽管存在一些限制,但当前 trait 中的 async fn
版本可能离稳定化不远了。5您可以通过关注跟踪问题来关注进度。
首先要回答两个大问题
如果您是异步 Rust 爱好者并且愿意尝试实验性的新功能,我们非常欢迎您试用!
如果您使用 #[async_trait]
,您可以尝试从一些不使用动态分发的 trait(及其 impls)中删除它。或者如果您正在编写新的异步代码,请尝试在那里使用它。无论哪种方式,您都可以在上面的链接中告诉我们您的体验。
结论
这项工作得以实现,归功于许多人的努力,包括
- Michael Goulet
- Santiago Pastorino
- Oli Scherer
- Eric Holk
- Dan Johnson
- Bryan Garza
- Niko Matsakis
- Tyler Mandry
此外,它建立在多年编译器工作的基础上,使我们能够发布 GATs 以及其他基本的类型系统改进。我们非常感谢所有做出贡献的人;没有你们的贡献,这项工作是不可能完成的。谢谢!
要了解有关此功能及其背后的挑战的更多信息,请查看trait 中的静态 async fn RFC以及为什么 trait 中的 async fn 很难。此外,请继续关注后续文章,我们将探讨使表达 Send
bounds 而无需特殊 trait 的语言扩展。
感谢 Yoshua Wuyts、Nick Cameron、Dan Johnson、Santiago Pastorino、Eric Holk 和 Niko Matsakis 审阅了本文的草稿。
-
此功能称为 “type alias impl trait”。↩
-
编译器生成的实际错误消息比这更嘈杂一些,但这将会得到改进。↩
-
Send
和Sync
等自动 trait 在这方面是特殊的。编译器知道print_all
的返回类型只有在其参数的类型为Send
时才为Send
,并且与常规 trait 不同,它允许在类型检查程序时使用此知识。↩ -
请参阅倡议网站上的 dyn trait 中的 Async fn,以及本系列中的第 8 篇和第 9 篇文章。↩
-
什么时候?可能在未来六个月左右的某个时间。但不要以此为准 :)↩