Rust 中的异步编程

Rust 选择了 async/await 作为其异步编程模型。async/await 模型性能高,还能支持底层编程,它的问题是内部实现机制有些复杂,理解和使用起来也不够简单。

在心智模型一节中,我们将完全抛开 Rust 去理解 async/await 模型。在实现一节,我们介绍在 Rust 中实现该模型时,必须引入的一些复杂性。在使用方法一节中,介绍如何在 Rust 中使用异步编程。

Future

心智模型

一个问题被解决可能需要经历多个阶段,一个阶段到下一个阶段之间需要等待特定外部消息的到来。可以为每一个等待的点创建一个状态,还有一个最终状态。于是一个完整的任务被拆分成几个阶段、几个中间状态,称被拆分后的任务为一个 Future。

实现

一个简单的 future 实现可能像这样:

1
2
3
4
trait SimpleFuture {
type Output;
fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}

实现 future 特征的结构体可能像是这样:

1
2
3
4
5
6
7
8
9
10
11
12
struct AsyncFuture {
fut_one: FutOne,
fut_two: FutTwo,
state: State,
}

// `async` 语句块可能处于的状态
enum State {
AwaitingFutOne,
AwaitingFutTwo,
Done,
}

Runtime

Runtime 就是让 Future 运行起来的设施。现在来看一种 Futures 运行的工作流模型,这个模型包括两个异步的部分:Executor 和 Reactor,还有一个联系二者的结构 Waker。分离、模块化,抽象。这样设计有很自然的理由:

唤醒 Future 的方式是怎样的?这个问题应该让 Excutor 回答

让谁来唤醒 Future?这个问题应该让 Future 自己回答,所以将 waker 具体传递给哪个 reactor 应该在 Future 内部实现

从外部看,一个 Future 可能有三种状态:

  • 可执行
  • 挂起
  • 完成
1
2
3
4
enum Poll<T> {
Ready(T),
Pending,
}

Waker

有了 Waker,你可以组合不同的 Executor 和 Reactor。

Reactor 是通过操作系统提供的 IO 多路复用机制来完成,例如 Linux 中的 epoll,FreeBSD 和 macOS 中的 kqueue ,Windows 中的 IOCP, Fuchisa中的 ports 等,可以通过 Rust 的跨平台包 mio 来使用。

Waker 的实现需要考虑很多因素

Executor

如果还有 Future 未完成,Executor 会从就绪队列中取出一个 Future,然后开始以下三步:

  1. 从之前的 Future 状态开始执行,完成一个阶段到下一个状态
  2. 通过 Waker 向 Reactor 注册等待的消息,然后挂起这个 Future
  3. 检查消息队列,将可以执行的 FUture 加入就绪队列

Reactor

Reactor 需要做的事情很简单,就是当 Future 感兴趣的消息通过 Waker 发送给 Executor 就好了。

使用方法

Rust 官方只提供了关于 Future 的内容,而没有太多的关于 Runtime 的内容

Rust 提供了关键字 async 可以将一个程序块转化为一个 Future,这个过程发生在编译时,所以没有额外的运行时开销。在一个被 async 标记的程序块内,可以在一个 Future 后使用 .await,这样可以将一些 Futures 嵌套组合成一个大 Future。使用 .await 的方式的这些内部 Future 是同步的,如果想异步的话,可以使用 futures 包提供的一些宏。

Tokio 库中提供了一些 Executor 和 Reactor。