2024-10-20 23:44:05 +08:00
# Async 和 await
2024-10-21 17:55:39 +08:00
> [ch17-00-async-await.md](https://github.com/rust-lang/book/blob/main/src/ch17-00-async-await.md)
> <br>
> commit 3111eda07a4a4692bf69e3aaad999d840ac9c138
很多我们要求计算机处理的操作都需要一定的时间才能完成。例如,如果你使用视频编辑器来创建一个家庭聚会的视频,视频导出可能会花费从几分钟到几个小时的时间。类似地,下载由你家庭成员分享的视频也可能花费很长时间。如果能在等待这些长时间运行的操作完成之前做点其他事就太好了。
视频导出会尽可能使用所有的 CPU 和 GPU。如果你只有一个 CPU 核,同时操作系统在导出完成前也不会暂停,那么在其运行期间你无法使用计算机进行任何其他操作。但是这将是非常糟糕的体验。相反计算机的操作系统可以(也确实可以)隐式地中断导出足够长的时间来让你同时完成其他操作。
下载文件则有所不同。它不占用非常多的 CPU 时间。相反 CPU 需要等待来自于网络的数据。虽然可以在部分数据就绪时就开始读取,等待余下的数据也可能花费很长时间。即便数据全部就绪了,视频文件也可能非常大,所以完全加载也可能花费一些时间。虽然这可能就是一两秒,不过这对于一个现代处理器来说也是非常长的时间。如果能让 CPU 在等待网络调用完成的同时去处理别的工作就好了。所以同上操作系统会隐式地中断你的程序以便其它工作可以在网络操作进行的同时继续。
> 注意:视频导出这类操作通常被称为 “CPU 密集型”( “CPU-bound”) 或者 “计算密集型”( “compute-bound”) 操作。其受限于计算机 *CPU* 或 *GPU* 处理数据的能力,和其所能利用的速度。下载视频这类操作通常被称为 “IO 密集型”( “IO-bound”) 操作, 因为其受限于计算机 “输入输出” 的速度。它最多只能与通过网络收发数据的速度一样快。
在上述两个例子中,操作系统的隐式中断提供了一种形式的并发。不过这种并发只发生在整个程序的级别:操作系统中断一个程序并让其它程序继续处理。在很多场景中,因为我们能比操作系统在更为细的粒度上理解我们的程序,所以我们可以观察到很多操作系统无法察觉的并发机会。
2024-10-22 00:01:15 +08:00
例如,如果我们在构建一个管理文件下载的工具,我们应该以一种开始一个下载任务时不会锁定 UI 的方式来编写程序,并且用户应该能够同时开始多个下载。不过很多操作系统与网络交互的 API 都是 *阻塞* 的(*blocking*)。也就是说这些 API 在数据完全就绪之前阻塞程序的运行。
2024-10-21 17:55:39 +08:00
> 注意:如果你仔细思索一下,会发现这是 *大部分* 函数调用的工作方式!不过我们通常将 “阻塞” 这个术语保留给那些与文件、网络或其它计算机资源交互的函数调用,因为这些是单个程序可以从 *非* 阻塞操作中获益的地方。
我们可以新建专用的线程来下载每个文件以免阻塞主线程。然而,最终我们会发现这些线程的开销会成为一个问题。如果这些调用在最开始就是非阻塞的就更好了。最后,如果能使用阻塞代码中那样直接的风格编写非阻塞代码就更好了。比如这样:
```rust,ignore,does_not_compile
let data = fetch_data_from(url).await;
println!("{data}");
```
这正是 Rust async 抽象所提供的。不过在讲解它们在实践中如何工作之前, 让我们稍微绕个远路来了解一下并行( parallelism) 和并发( concurrency) 的区别。
### 并行与并发
在上一章中,我们将并发和并行大体上视为可相互替换的。现在我们需要更精确的区分它们,因为在我们开始讲解后它们的区别就会显现出来。
思考一下不同的团队分割方法来开发一个软件项目。我们可以分配给一个个人多个任务,或者可以每个团队成员一个任务,或者可以采用这两种方法的组合。
当一个个人在任何一个任务完成前同时处理多个任务,这就是 *并发* 。可能你在计算机上开着两个不同的项目,当你对一个项目感到厌烦或者遇到困难时,可以切换到另一个项目。因为你是单独一个人,所以无法真正同时进行两个任务,但是你可以多任务处理,通过切换来处理多个任务。
< figure >
< img alt = "并发工作流" src = "img/trpl17-01.svg" class = "center" / >
< figcaption > 图 17-1: 一个并发工作流, 在任务 A 和任务 B 之间切换< / figcaption >
< / figure >
当你同意将一组任务在组员中分配,每一个组员分配一个任务并单独处理它,这就是 *并行* 。每个组员可以真正同时进行工作。
< figure >
2024-10-22 00:01:15 +08:00
< img alt = "并发工作流" src = "img/trpl17-02.svg" class = "center" / >
2024-10-21 17:55:39 +08:00
< figcaption > 图 17-2: 一个并行流, 其中任务 A 和任务 B 的工作同时独立进行< / figcaption >
< / figure >
2024-10-22 00:01:15 +08:00
在这两种场景中,你可能需要协调不同的任务。可能你 *认为* 某个人所作的任务与其他人的工作完全不相关,不过它确实需要其他组员的工作完成才能进行。一些工作可以并行进行,不过一些工作事实上是 *串行* 的:它们只能串行地发生,一个接着一个,如图 17-3 所示。
< figure >
< img alt = "并发工作流" src = "img/trpl17-03.svg" class = "center" / >
< figcaption > 图 17-3: 一个部分并行的工作流, 其中任务 A 和任务 B 的工作相互独立,直到任务 A3 阻塞在等待任务 B3 的结果< / figcaption >
< / figure >
同理,你可能会意识到你自己的一个任务依赖另一个任务。现在并发任务也变成串行的了。
并行与并发也可以相互交叉(阻塞)。如果你得知某个同事卡在等待你的一个任务完成,你可能会集中所有精力在这个任务上来 “解锁” 你的同事。你和你的同事则不再能并行地工作了,同时你也不能够并发地处理自己的任务。
同样的基础动态也作用于软件与硬件。在一个单核的机器上, CPU 一次只能做一个操作, 不过它仍然可以并发工作。使用像线程、进程和异步( async) 这样的工具, 计算机可以暂停一个活动, 并在最终切换回第一个活动之前切换到其它活动。在一个有多个 CPU 核心的机器上,它也可以并行工作。一个核心可以做一件工作的同时另一个核心可以做一些完全不相关的工作,而且这些工作实际上是同时发生的。
当使用 Rust 中的 async 时, 我们已经在处理并发。取决于硬件、操作系统和所使用的异步运行时( async runtime) -- 之后会介绍更多的异步运行时!并发也可能在底层使用了并行。
现在让我们深入理解 Rust 的异步编程实际上是如何工作的!在剩下的章节,我们会:
- 学习如何使用 Rust 的 `async` 和 `await` 语法
- 探索如何使用异步模型来解决第十六章中遇到的一些挑战
- 了解多线程和异步如何互补,在很多场景中你甚至可以同时使用两者