异步和等待
我们要求计算机执行的许多操作可能需要一段时间才能完成。例如,如果您使用视频编辑器创建了一个家庭庆祝活动的视频,导出它可能需要几分钟到几小时。同样,下载家人分享的视频也可能需要很长时间。如果我们能在等待这些长时间运行的进程完成时做其他事情,那将是非常好的。
视频导出将尽可能多地使用CPU和GPU的性能。如果你只有一个CPU核心,而且你的操作系统在导出完成之前从未暂停过该过程,那么在它运行期间你将无法在计算机上做任何其他事情。这将是一个相当令人沮丧的体验。相反,你的计算机的操作系统可以——而且确实做到了!——在足够频繁的情况下无形地中断导出,以便让你在过程中完成其他工作。
文件下载是不同的。它不会占用太多的CPU时间。相反,CPU需要等待数据从网络到达。虽然一旦有部分数据到达,你就可以开始读取这些数据,但其余部分可能需要一段时间才会出现。即使所有数据都已到达,视频也可能相当大,因此可能需要一些时间来加载所有内容。也许只需要一两秒——但对于现代处理器来说,这已经是非常长的时间了,因为它们每秒可以执行数十亿次操作。能够在等待网络调用完成的同时,让CPU用于其他工作会很好——因此,你的操作系统会无形中中断你的程序,以便在网络操作仍在进行时可以发生其他事情。
注意:视频导出是一种常被描述为“CPU密集型”或“计算密集型”的操作。它受限于计算机在CPU或GPU中处理数据的速度,以及能够利用多少这种速度。视频下载是一种常被描述为“IO密集型”的操作,因为它受限于计算机的输入和输出速度。它只能以数据在网络中传输的速度进行。
在这两个例子中,操作系统的不可见中断提供了一种并发形式。这种并发仅在整个程序的层面上发生:操作系统中断一个程序以让其他程序完成工作。在许多情况下,由于我们对程序的理解比操作系统更细致,因此可以发现许多操作系统无法看到的并发机会。
例如,如果我们正在构建一个管理文件下载的工具,我们应该能够以这样的方式编写程序:启动一个下载不会锁定UI,并且用户应该能够同时启动多个下载。许多操作系统用于与网络交互的API是阻塞的。也就是说,这些API会阻塞程序的进度,直到它们处理的数据完全准备好。
注意:这其实是大多数函数调用的工作方式,如果你仔细想想!然而, 我们通常将“阻塞”一词保留给与文件、网络或其他计算机资源交互的函数调用,因为这些地方的单个程序会从非阻塞操作中受益。
我们可以通过创建一个专门的线程来下载每个文件,从而避免阻塞主线程。然而,我们最终会发现这些线程的开销是一个问题。如果调用本身不阻塞也会更好。最后但同样重要的是,如果我们能够用与阻塞代码相同的直接风格编写代码会更好。类似于这样:
let data = fetch_data_from(url).await;
println!("{data}");
这就是 Rust 的 async 抽象给我们的。在我们看到这如何在实践中工作之前,我们需要简要了解一下并行性和并发性的区别。
并行性和并发性
在上一章中,我们将并行性和并发性视为基本可以互换的概念。现在我们需要更精确地区分它们,因为这些差异将在我们开始工作时显现出来。
考虑团队在软件项目中分配工作的不同方式。我们可以分配给单个个体多个任务,或者每个团队成员分配一个任务,或者两种方法的混合。
当一个人在任何任务完成之前就开始处理多个不同的任务时,这称为并发。也许你在计算机上有两个不同的项目,当你对一个项目感到无聊或卡住时,你会切换到另一个项目。你只是一个个体,所以你不能在同一时间在两个任务上都取得进展——但你可以多任务处理,通过在任务之间切换来在多个任务上取得进展。
当你同意将一组任务在团队成员之间分配,每个人承担一个任务并独自完成时,这被称为并行性。团队中的每个人可以同时取得进展。
在这两种情况下,你可能需要在不同的任务之间进行协调。也许你认为某个人正在做的任务与其他人的工作完全独立,但实际上它需要团队中另一个人完成的工作。有些工作可以并行完成,但有些工作实际上是串行的:只能按顺序进行,一个接一个,如图17-3所示。
同样,您可能会意识到自己的一个任务依赖于另一个任务。现在,您的并行工作也变成了串行。
并行性和并发性也可以相互交织。如果你得知一位同事在你完成某项任务之前无法继续工作,你可能会集中所有精力完成那项任务以“解除”同事的阻塞。你和你的同事不再能够并行工作,你也无法再同时处理自己的任务。
相同的基本身体动力学也适用于软件和硬件。在具有单个CPU核心的机器上,CPU一次只能执行一个操作,但它仍然可以并发工作。使用线程、进程和异步等工具,计算机可以暂停一个活动并切换到其他活动,最终再回到第一个活动。在具有多个CPU核心的机器上,它还可以并行工作。一个核心可以做一件事,而另一个核心可以做完全不相关的事情,这些实际上同时发生。
当在 Rust 中使用 async 时,我们总是在处理并发。根据硬件、操作系统以及我们使用的异步运行时——稍后会详细介绍异步运行时!——这种并发在底层也可能使用并行。
现在,让我们深入探讨 Rust 中的异步编程实际上是如何工作的!在本章的其余部分,我们将:
- 了解如何使用 Rust 的
async
和await
语法 - 探索如何使用异步模型来解决我们在第16章中讨论的一些相同挑战
- 看看多线程和异步如何提供互补的解决方案,你甚至可以在许多情况下将它们一起使用。