# 异步编程基础：异步、等待、未来值与流

许多我们要求计算机执行的操作，都会需要一段时间才能完成。如果我们能在等待这些耗时较长的进程完成时做点别的事情，那就再好不过了。现代计算机提供了两种同时处理多个操作的技术：并行和并发，parallelism and concurrency。然而，我们的程序逻辑大多是以线性方式编写的。我们希望能够在无需事先精确指定每段代码的运行顺序和运行方式下，指定程序应该执行的操作，以及某一函数可以暂定而程序的某个其他部分可以运行的点。所谓 *异步编程*，属于一种抽象，让我们可以根据为我们处理协调细节的潜在的暂停点和最终结果，来表达代码。

这一章在第 16 章中线程在并行和并发中的使用基础上，引入另一种编写代码的方法： Rust 的未来值、流与 `async` 和 `await` 语法，二者让我们可以表达操作如何得以称为异步，以及一些实现了异步运行时的第三方代码箱，所谓异步运行时，是指管理和协调异步操作的代码。

我们来设想一个示例。假设咱们要导出一段家庭庆祝活动的视频，一个可能耗时数分钟到数小时不等的操作。视频导出将使用尽可能多的 CPU 和 GPU。若咱们只有一个 CPU 核心，且操作系统在导出完成前不会暂停该导出 -- 也就是说，如果他 *同步地* 执行导出，那么在该任务运行期间，咱们将无法在计算机上执行其他任何操作。这将是非常令人沮丧的体验。幸运的是，咱们计算机的操作系统可以，而且确实会足够频繁地以无形的方式（无感地）中断导出，以让咱们可以同时完成其他工作。

现在，假设咱们正在下载其他人共享的视频，这也会需要一段时间，但不会占用太多 CPU 时间。在这种情形下，CPU 必须等待来自的数据到达。虽然咱们可以在数据一到达时就开始读取，但可能需要一些时间才能显示所有数据。即使数据已全部到位，但若视频很大，加载全部数据也可能需要至少一两秒。这听起来可能不多，但对于每秒可执行数十亿次运算的现代处理器来说，这已是相当漫长的时间。同样，咱们的操作系统将无形中中断程序，以允许 CPU 在等待网络调用完成的同时执行其他工作。

视频导出是 *CPU 密集，CPU-bound* 或 *计算密集，compute-bound* 操作的一个示例。他受限于计算机 CPU 或 GPU 的潜在数据处理速度，以及其中有多少速度可用于该操作。视频下载是 *IO 密集，IO-bound* 操作的一个示例，因为他限于计算机的 *输入和输出* 速度的限制；他只能以通过网络发送数据的速度运行。

在这两个示例中，操作系统的不可见中断机制，提供了一种并发形式。不过，这种并发仅发生在整个程序的级别：操作系统会中断一个程序，以让其他程序得以完成工作。在许多情况下，由于我们对咱们程序的理解比操作系统更细粒度，因此我们可以发现操作系统无法察觉的并发机会。

例如，若我们正在构建一个管理文件下载的工具，我们就应该能够将咱们的程序，编写成启动下载不会锁定用户界面，以及用户应能够同时启动多个下载。然而，许多用于网络交互的操作系统 API 都是 *阻塞式的*；也就是说，这些 API 会阻塞程序的进程，直到正在处理的数据完全准备就绪为止。


> **注意**：若咱们仔细想想，这正是 *大多数* 函数调用的工作方式。不过，*阻塞* 这一术语通常保留给与文件、网络或计算机上其他资源交互的函数调用，因为在这些情况下，单个程序将受益于 *非阻塞，non-blocking* 的操作。


我们可以通过生成专用线程来下载每个文件，避免阻塞主线程。然而，这些线程使用的系统开销最终会成为一个问题。若调用一开始就不阻塞，进而我们可以定义一些我们希望程序完成的任务，并允许运行时选择运行这些任务的顺序和方式，那就更好了。

这正是 Rust 的 `async`（异步，*asynchronous* 的缩写）抽象给予我们的。在这一章中，咱们将了解有关异步的所有内容，我们将介绍以下主题：

- 怎样使用 Rust 的 `async` 与 `await` 语法，并通过某种运行时执行异步函数；
- 如何使用异步模型解决我们在第 16 章中探讨过的一些同样挑战；
- 多线程和异步如何提供互补的解决方案，咱们可以在许多情形下结合使用。


不过，在了解异步在实践中的工作原理前，我们需要先绕道讨论一下并行和并发之间的区别。


## 并行与并发

到目前为止，我们基本上视并行和并发为可以互换的概念。现在我们需要更准确地区分他们，因为随着我们开始工作，差异就将显现出来。

请设想某个团队在某一软件项目上分工的不同方式。咱们可以给单个成员分配多项任务，也可以给每名成员分配一项任务，或者混合使用这两种方式。

当一个在多个不同任务中的任何一个完成前，就处理这些任务时，这就是 *并发*。实现并发的一种方式，类似于在计算机上检出两个不同项目，当咱们对一个项目感到无聊或卡住时，就切换到另一项目。咱们毕竟只有一个人，所以咱们不能同时在两项任务上取得进展，但咱们可以进行多任务处理，通过在任务之间切换，一次在一个任务上取得进展（见图 17-1）。

<a name="f_17-1"></a>
![并发工作流程，在任务 A 和任务 B 之间切换](./images/trpl17-01.svg)

**图 17-1**：并发工作流程，在任务 A 和任务 B 之间切换


当团队通过让每名成员承担一项任务并独立完成，来拆分一组任务时，这就是 *并行*。团队中的每个人都可以在同一时间取得进展（见图 17-2）。

<a name="f_17-2"></a>
![并行工作流程，任务 A 和任务 B 的工作独立进行](./images/trpl17-02.svg)

**图 17-2**：并行工作流程，其中任务 A 和任务 B 独立进行

在这两种工作流程中，咱们可能都必须在不同任务之间协调。也许咱们以为分配给某人的任务完全独立于其他人工作的，但实际上他需要团队中的另一人先完成自己的任务。有些工作可以并行完成，但有些工作实际上是 *串行的*：他只能按顺序依次进行，一项任务接着另一项任务，如下图 17-3 中所示。

<a name="f_17-3"></a>
![部分并行的工作流程，任务 A 和任务 B 的工作独立进行，直到任务 A3 被任务 B3 的结果阻断。](./images/trpl17-03.svg)

**图 17-3**：部分并行的工作流程，其中任务 A 与任务 B 上的工作独立进行，直到任务 A3 因任务 B3 的结果而阻塞

同样，咱们可能会发现咱们自己的一项任务依赖于咱们自己的另一项任务。现在咱们的并发工作也变成了串行的。

并行和并发也会相互交叉。当咱们得知某名同事因咱们尚未完成某项任务而陷入停滞时，咱们就可能会把全部精力集中于该项任务，以 “解锁” 咱们的同事。咱们和咱们的同事不再能够并行地工作，咱们也不再能够并发地处理自己的任务。

同样的基本运作机制适用于计算机软件和硬件。在单 CPU 核心的机器上，CPU 一次只能执行一项操作，但他仍可以并发地工作。使用线程、进程及异步等工具，计算机可以暂停一项活动并切换到其他活动，然后最终再次循环到第一项活动。在有着多个 CPU 核心的机器上，他还可以并行地工作。一个核心可以执行一项任务，而另一个核心则执行完全不相关的任务，并且这些操作实际上同时发生的。

在 Rust 中运行异步代码通常是并发进行的。根据硬件、操作系统以及我们所使用的异步运行时（稍后将详细介绍异步运行时）的不同，这种并发也在底层也可能使用并行。

现在，我们来深入探讨 Rust 中的异步编程，就是是如何工作的。


