在 2022 年,异步 Rust 将如何发展

本文第一部分翻译自 Async Rust in 2022

在大概一年前,异步工作组一块开始编写一份文档,去描述异步发展的愿景。在 2022 年,这份文档中的一些工作已经有所进展,根据这些进展,我们想修订一下这份文档。

使用 Rust 2024 编写 Issue 聚合器

想象一下,我们已经有了 2024 年的标准 Rust,你现在打算使用 Rust 编写你的第一个项目。平时工作时你用 Github 管理代码,所以你想有一个工具,能够遍历项目中的所有 Issue 并自动分类。你准备使用异步。你先下载了 Rust Book,睿智的眼神看向了异步 IO 章节。这个章节里展示了异步 Rust 程序的基本结构,和其他 Rust 程序差不多,异步代码同样以 main 打头,但这次前面变成了 async fn

1
2
3
async fn main() {
...
}

继续打开 crates.io,你搜索了「github」,找到了一个漂亮的(nifty) crate crabbycat,正是用来获取 Github Issue 的。你导入了这个包,然后继续闷头工作。首先,你需要遍历所有的 Issue。

1
2
3
4
5
6
7
async fn main() {
for await? issue in crabbycat::issues("https://github.com/rust-lang/rust") {
if meets_criteria(&issue) {
println!("{issue:?}");
}
}
}

你的 crate 写完了,看起来跑得还行,你发了条推展示了下这玩意。过了一会儿,居然真的有用户开了个 PR 给这程序加了 GitLab 的支持。为了达到目的,他们编写了一个 trait,让爬取 Issue 部分的代码可以泛型编程。这个 trait 只有一个返回迭代器的方法 issues

1
2
3
4
5
6
7
8
9
10
11
trait IssueProvider {
async fn issues(&mut self, url: &str)
-> impl AsyncIterator<Item = Result<Issue, Err>>;
}

#[derive(Debug)]
struct Issue {
number: usize,
header: String,
assignee: String,
}

现在,他们能够重构你写的代码了,能够用这个 IssueProvider 来让程序更加通用。他们决定用 dyn 来避免写出同构代码。

1
2
3
4
5
6
7
fn process_issues(provider: &mut dyn IssueProvider) {
for await? issue in provider.issues("https://github.com/rust-lang/rust") {
if meets_criteria(&issue) {
println!("{issue:?}");
}
}
}

这可真不错。你合并了这个分支。之后,有个人又想把你的程序移植到 Chartreuse 系统上。这个系统是基于 Actor 模型的,有自己的一套异步运行时。幸运的是,你完全不用鸟这些事情。你所有代码的底层运行时都能无缝切换为 Chartreuse 的运行时。

同时,在 2022 年……

当然,现在还是 2022 年。上面这些东西都不是真的——至少现在还不是。为了让我们能够像这样写代码,还有很多 RFC 和实现工作需要完成:

  • IssueProvider 需要在 trait 里声明异步方法。
  • 把函数参数声明成 &mut dyn IssueProvider 需要支持在 trait 里动态分发异步函数。
    • 而且还得返回动态的 impl AsyncIterator
  • for await? 让我们能够顺利地在异步代码里使用循环。
  • 标准库里地异步函数仍有不同名字,还没有标准化,而且一旦我们实现了 trait 中的异步函数,它的定义很有可能改。
  • 编写 async fn main 并且支持切换运行时要求程序可移植。

随着这些工作的进行,未来可能会有很多细节需要修改,有些部分也可能不值得费工夫去做。如果真没有这些事,至少生成器的语法就是一个还在激烈争论(hotly contested)的话题。但是宏观上有一点我们不会改:编写异步代码就应该和编写同步代码一样简单,除了偶尔出现的 async await

如何达到目标

我们把异步工作组组织为了数个不同的计划(initiatives),每个都负责推进愿景中的一部分。接下来的是一份列表,展示了近期活跃的小组和从他们建立开始几个月间的进展。

异步基础计划组

由 tmandry 领导,现正推进 trait 中的异步函数。

  • 我们一直在协调(coordinate)和支持 GATs 与 impl trait 工作组。
  • 我们还引入(land)了 RFC 描述 trait 中的静态异步函数,编写了一份在返回语句中使用 impl trait 的 RFC 草案。
  • 我们正在设计动态分发。现在的进度可参照这篇文章
  • 其他方面我们也一直在努力,比如对上下文和功能性的建议

异步迭代计划组

由 estebank 领导,探索生成器和异步生成器。

  • Estebank 现在草拟(prototype)了一个生成器的过程宏,大家正在讨论语法和其他细节。

可移植性计划组

由 nrc 领导,从 AsyncRead AsyncWrite 等标准化的 trait 开始,探索让代码可以在不同运行时间移植的代价。

  • nrc 写了一份博客,描述了愿景。

打磨计划组

由 eholk 领导,专注于打磨改进现有的功能。所有小的改进会形成大的不同。

  • 我们提交了一个 PR,改进了生成器捕获分析中当变量在 yield 前被移动的情况。以及另一个 PR,收紧了临时作用域来避免不必要的生成器捕获。
  • Gus Wynn 做出了重要的工作,添加了 must_not_suspend 规则去检查本不应该存活超过异步(调度)点的值。
  • 我们正在寻找办法,让异步栈追踪能更易读和有用点。

工具计划组

由 pnkfelix 领导,负责支持在异步生态中编写帮助异步编程工具的人的人。

  • Michael Woerister 正在探索异步崩溃转储文件的恢复,从而提供一种机制来恢复和检查异步程序的状态。
  • Eliza Weisman 和其他很多人近期共同发布了 tokio console0.1 版本。Tokio Console 是一个异步程序诊断 debug 工具。它可以提供异步运行时的动态状态,也能在检测到与 bug 或性能有关的可疑行为时发出警告。

在我们的工作路线中,你能找到一份完整的工作列表,这里也有一些我们已经推进的成果。

想要出一份力吗?

如果你想出份力,好的起步是先阅读打磨计划组的页面。你也许也会想参加一下打磨计划组的每周会议


为什么关心 Rust 异步

下面是我自己的碎碎念,关于为什么这么关心 Rust 的异步发展。

Rust 从诞生的时候,就标榜自己无运行时,能够实现 zero-cost 的高级抽象,再加上奇妙的所有权内存模型,在很多语言特性上 Rust 需要面临的任务要比动态语言和有大运行时的静态语言(比如 Go)艰巨得多。

Rust is blazingly fast and memory-efficient: with no runtime or garbage collector, it can power performance-critical services, run on embedded devices, and easily integrate with other languages.

—— Rust

所以,即使单纯地关注 Rust 该如何抽象好这本就需要运行时支持的异步功能,就已经很有意思了。

再就是在实现该功能的路上,Rust 需要解决的问题远超过该任务本身。可以看到,在第一个工作组的任务中提及了这样两个东西。

  1. GATs
  2. trait 中的异步函数签名

第一点在先前的文章中有提到过,它同时是 Rust 实现完整函数式编程支持上的一个障碍。到目前为止,Rust 并不完全支持函数式编程。观察下文代码。

1
2
3
4
5
6
trait Functor {
type Unwrapped;
fn map<F>(self, f: F) -> Self
where
F: FnMut(Self::Unwrapped) -> Self::Unwrapped;
}

这是一个 Functor,对应着 map 操作。现在我们就很难讲这个 Unwrapped 该怎么处理。例如当为 Option<A> 实现 Functor 时,Unwrapped 由于 map 签名返回的 Self 而必须定义为 A,那么实现的 map 操作的 f 只能接受 A -> A 的函数,这显然是违背 Functor 定义的。

那要是这么写呢?

1
2
3
4
5
6
7
trait Functor {
type Unwrapped;
fn map<F, R>(self, f: F) -> R
where
F: FnMut(Self::Unwrapped) -> R
R: Functor;
}

这依然不对,因为我们无法保证 map 之后外面的 box 是不是和以前一样,例如我完全可以同时实现 Option<A> -> Option<A> Option<A> -> Result<A> 而不受任何阻拦,这同样违背 Functor 定义。

所以,为了同时满足这两种情况,代码需要改成:

1
2
3
4
5
6
7
trait Functor {
type T;
type Wrapper<R>: Functor;
fn map<F,R>(self, f: F) -> Self::Wrapper
where
F: FnMut(Self::T) -> R;
}

这个 Wrapper 的类型就涉及到了 GATs。在像 Rust 这样复杂的类型系统里解决这项工作用脚趾头想想就很复杂。你可能会说这种问题只有在高度抽象的代码中才会出现,也许可以搁置,但其实因为 Rust 的生命周期同样是类型,trait 中出现 GATs 几乎是不可避免的(下面就会立刻介绍一个例子)。而在推进异步的路上,这个问题必须被解决。

至于 trait 中的异步签名,可以说是击到了 Rust 类型系统的一个痛点。async 实际上是这样一个语法糖

1
2
async fn func() -> R;
fn func() -> impl Future<Output = R> + '_;

但是 trait 中不支持 impl 静态分发(他们也正在实现这个)。这也是 Rust zero-cost 所做的妥协,impl 的分发是在编译期完成的,但现在的 trait 系统做不到。第一个原因还是 GATs 的缺失。async 需要捕获当前的上下文,捕获上下文就需要生命周期,函数不会只在一个地方使用,这种生命周期就显然不会只有一个,那么 trait 中的 async fn 就必须支持 GATs。

第二个原因则更加难以解释:「谁知道你这个 async fn 返回的类型到底是什么?」就算解决了 GATs,我们照样不应该知道这个编译器自动生成的返回类型到底是个什么玩意,我们就不该知道这个语法糖展开的样子,但现在你必须知道它的类型才有可能继续写代码:假设我们想约束一个 trait 中的 async fn 返回的值需要能够 Clone,该怎么写?

1
2
3
4
5
trait AsyncFn{
async get_content() -> Content;
}

let res = a.get_content(); // 注意我没有 await

你总不能写 Content: Clone 吧?我想要的可是去限制这玩意——impl Future<Output = R> + '_ 能 Clone。它是 impl,所以是有静态类型的,先不提我们本不知道编译器怎么命名这个内部类型,去写这么一个签名本身就是折磨,并且一旦需要用户自己处理这个未知的类型,就会和官方博客中的宏观愿景相冲突

……

还有很多问题需要解决。总之,这条路一定是艰辛的。限于能力,我自身没有什么系统的类型知识,也没有什么语言设计功底,顶多也就是自己写过小的 C 编译器,所以也没有精力和底蕴提出什么合适的解决方案。能做的就是把这些问题摆出来,让我认识到自己的浅薄,让我认识到设计一门语言的难度,让我认识到世界上的人在做什么富有激情和挑战性的工作。同样,也能给其他对 Rust 感兴趣的人看一看。

纸上得来终觉浅,绝知此事要躬行。


在 2022 年,异步 Rust 将如何发展
https://blog.chenc.me/2022/02/18/async-rust-in-2022/
作者
CC
发布于
2022年2月18日
许可协议