背景

Rust 中的 Futures 类似于 Javascript 中的promise[1],它们是对 Rust 中并发原语的强大抽象。这也是通往async/await[2]的基石,async/await 能够让用户像写同步代码一样来写异步代码。

Async/await 在 Rust 初期还没有准备好,但是这并不意味着你不应该在你的 Rust 项目中开始使用 futures。tokio[3] crate 稳定、易用且快速。请查看此文档[4]来了解使用 future 的入门知识。

Futures 已经在标准库当中了,但是在这个系列的博客中,我打算写一个简化版本来展示它是如何工作的、如何使用它以及避免一些常见的陷阱。

Tokio 的主分支正在使用 std::future,但是所有的文档都引用自 0.1 版本的 futures。不过,这些概念都是适用的。

尽管 futures 现在在 std 当中,但是缺失了很多常用的特性。这些特性当前在 future-preview[5] 中维护,并且我将会引用定义在其中的函数和 trait。事情进展得很快,那个 crate 里的很多东西最终都会进入标准库。

预备知识

了解一些 Rust 的知识或者在接下来的过程中愿意去学习 Rust(能够阅读Rust book[6]就更好了。)

一个现代的浏览器,比如 Chrome,FireFox,Safari,或者 Edge(我们将会使用 rust playground[7])

就这些!

目标

本文的目标是能够理解下面的代码,并且实现所需的类型和函数来使其能够编译。这段代码对于标准库的 futures 是有效的语法,并且说明链式 futures 是如何工作的。

// 这段代码目前还不能编译
fn main() {
let future1 = future::ok::(1)
.map(|x| x + 3)
.map_err(|e| println!("Error: {:?}", e))
.and_then(|x| Ok(x - 3))
.then(|res| {
match res {
Ok(val) => Ok(val + 3),
err => err,
}
});
let joined_future = future::join(future1, future::err::(2));
let val = block_on(joined_future);
assert_eq!(val, (Ok(4), Err(2)));
}

Future 到底是什么?

具体来讲,它是一系列异步计算所代表的值。Futures crate 的文档称其为“表示一个对象,该对象是另一个尚未准备好的值的代理(a concept for an object which is a proxy for another value that may not be ready yet)”。

Rust 中的 futures 允许你定义一个可以被异步运行的任务,比如一个网络调用或者计算。你可以在那个结果上链接函数,对其进行转换,处理错误,与其他的 futures 合并以及执行许多其他的计算。这些函数只有当 future 被传递给一个 executor,比如 tokio 的run函数,才会执行。事实上,如果你在离开作用域之前没有使用 future,什么事都不会发生。也因此,futures crate 声明 futures 是must_use的,并且如果你允许它们没有被使用就离开作用域,编译器会给出一个警告。

如果你熟悉 JavaScript 的 promises,有些东西可能会觉得奇怪。在 JavaScript 中,promises 是在事件循环中被执行,并且没有其他的可以运行它们的选择。executor函数是立即运行的。但是,从本质上来讲,promise 仍然只是简单地定义了一系列将来要执行的指令。在 Rust 中,executor 可以选择许多异步策略中的任意一个来运行。

构建我们的 Future

从高一点的层次来讲,我们需要一些代码片段来让 futures 工作;一个 runner,future trait 以及 poll 类型。

首先,一个 Runner

如果我们没有一种方式来执行我们的 future,它将不会做什么事情。因为,我们正在实现我们自己的 futures,所以我们也需要实现我们自己的 runner。在这个练习中,我们实际上不会做任何异步的事情,但是我们将会进行近似的异步调用。

Futures 基于 pull 而不是基于 push。这使得 futures 能够成为一个零抽象,但是这也意味着它们会被轮询一次,并且在当它们准备能够再次轮询的时候负责提醒 executor。它工作方式的具体细节对于理解 futures 是如何被创建和链接到一起并不重要,因此,我们的 executor 只是一个非常粗略的近似。它只能运行一个 future,并且它不能做任何有意义的异步。Tokio 文档有很多关于 futures 运行时模型的信息。

下面是一个看起来非常简单的实现:

use std::cell::RefCell;
thread_local!(static NOTIFY: RefCell = RefCell::new(true));
struct Context {
waker: &'a Waker,
}
impl Context {
fn from_waker(waker: &'a Waker) -> Self {
Context { waker }
}
fn waker(&self) -> &'a Waker {
&self.waker
}
}
struct Waker;
impl Waker {
fn wake(&self) {
NOTIFY.with(|f| *f.borrow_mut() = true)
}
}
fn run(mut f: F) -> F::Output
where
F: Future,
{
NOTIFY.with(|n| loop {
if *n.borrow() {
*n.borrow_mut() = false;
let ctx = Context::from_waker(&Waker);
if let Poll::Ready(val) = f.poll(&ctx) {
return val;
}
}
})
}

run是一个泛型函数,其中 F 是一个 future,并且它返回一个定义在Future trait 中的Output类型的值,我们在后面会讲到它。

函数体的逻辑近似于一个真实的 runner 可能会做的事情,它会一直循环直到被提醒 future 准备好被再次轮询了。它会在 future 就绪时从函数返回。Context和Waker类型是对定义在future::task模块中的同名类型的模拟,可以在这里[8]看到。编译需要这里有它们的存在,但是这不再本文的讨论范围之内。具体它们是怎么实现的,你可以去自由探索。

Poll 是一个简单的泛型枚举,我们可以像下面这样定义它:

enum Poll {
Ready(T),
Pending
}

我们的 Trait

Trait[9]是在 Rust 中定义共享行为的一种方式。它允许我们能够指定实现类型必须定义的类型和函数。它还可以实现默认的行为,这会在我们讲到组合器(combinator)的时候看到。

我们的 trait 实现看起来像下面这样(这和真实的 futures 实现是一致的):

trait Future {
type Output;
fn poll(&mut self, ctx: &Context) -> Poll<:output>;
}

这个 trait 现在还很简单,只是声明了所需的类型——Output,以及唯一需要的方法的签名——poll,poll方法持有一个 context 对象的引用。这个对象持有一个对 waker 的引用,waker 被用于提醒运行时(runtime)future 准备好被再次轮询。

我们的实现

#[derive(Default)]
struct MyFuture {
count: u32,
}
impl Future for MyFuture {
type Output = i32;
fn poll(&mut self, ctx: &Context) -> Poll<:output> {
match self.count {
3 => Poll::Ready(3),
_ => {
self.count += 1;
ctx.waker().wake();
Poll::Pending
}
}
}
}

让我们一行一行地来看上面的代码:

#[derive(Default)] 为这个类型自动创建一个::default()函数。数值类型(即这里的count)默认为 0。

struct MyFuture { count: u32 }定义了一个带有一个计数器(count)的简单结构体。这让我们能够模拟异步行为。

impl Future for MyFuture 是我们对这个 trait 的实现。

我们把 Output 设置为i32类型,因此我们可以返回内部的计数。

在我们的poll实现中,我们基于内部的 count 字段决定要做什么、

如果它匹配了 33=>,我们返回一个带有值为 3 的Poll::Ready响应。

在其他情况下,我们增加计数器的值并且返回Poll::Pending

加上一个简单的 main 函数,我们可以运行我们的 future 了!

fn main() {
let my_future = MyFuture::default();
println!("Output: {}", run(my_future));
}

自己运行一下![10]

最后一步

这就是它的工作原理,但是没有真正地向你展示出 futures 的强大。所以,让我们创建一个超级便利的 future,用它来链接到任意任意可以加 1 的类型来进行加 1 操作,例如,MyFuture。

struct AddOneFuture(T);
impl Future for AddOneFuture
where
T: Future,
T::Output: std::ops::Add,
{
type Output = i32;
fn poll(&mut self, ctx: &Context) -> Poll<:output> {
match self.0.poll(ctx) {
Poll::Ready(count) => Poll::Ready(count + 1),
Poll::Pending => Poll::Pending,
}
}
}

这段代码看起来复杂但实际上非常简单。我会再次一行一行地来回顾:

struct AddOneFuture(T);这是一个泛型newtype[11]模式的示例。它让我们能够wrap其他的结构体并且添加我们自己的行为。

impl Future for AddOneFuture是一个泛型 trait 实现。

T: Future保证被 AddOneFuture wrap 的任意东西实现了 Future。

T::Item: std::ops::Add确保了Poll::Ready(value)表示的值有对应的+操作。

剩下的部分就很容易看懂了。它使用self.0.poll轮询内部的 future,贯穿上下文,并且根据结果要么返回Poll::Pending或者返回内部 future 的计数加 1——Poll::Ready(count + 1)

我们可以只更新main函数以使用我们的新的 future。

fn main() {
let my_future = MyFuture::default();
println!("Output: {}", run(AddOneFuture(my_future)));
}

自己运行一下![12]

现在,我们能够看到我们是如何使用 futures 把异步行为链接到一起。只需要几个简单步骤,就可以建立为 futures 赋予强大能力的链式函数(combinators)。

概要

Future 是一种利用 Rust 零成本抽象概念来实现良好可读性、快速的异步代码的强大方式。

Futures 行为和 JavaScript 以及其他语言中的 promise 很像。

我们已经学到了很多关于构建通用类型和一部分将行为链接到一起的内容。

接下来

在 part 2[13],我们将讨论组合器(combinators)。组合器,在非技术性方面,能够让你使用函数(比如回调函数)来构建一个新类型。如果你已经用过 JavaScript 的 promises,这些将会很熟悉。