Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


x-i18n: generated_at: “2026-03-01T14:42:50Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: f9cd7f4b2e03b4be88231c9dd2d6d0e10a4b770dc4f3033cf3e33a6e6a69550e source_path: ch17-00-async-await.md workflow: 16

异步编程基础:Async、Await、Future 和 Stream (Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams)

Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams

我们要求计算机执行的许多操作都需要一段时间才能完成。如果我们能在等待这些长时间运行的进程完成的同时做点别的事情,那就太好了。现代计算机提供了两种同时处理多个操作的技术:并行和并发。然而,我们的程序逻辑主要是以线性方式编写的。我们希望能够指定程序应执行的操作,以及函数可以暂停而让程序的其他部分代为运行的点,而不需要预先精确指定每段代码运行的顺序和方式。“异步编程 (Asynchronous programming)”是一种抽象,它让我们根据潜在的暂停点和最终结果来表达我们的代码,并为我们处理协调的细节。

Many operations we ask the computer to do can take a while to finish. It would be nice if we could do something else while we’re waiting for those long-running processes to complete. Modern computers offer two techniques for working on more than one operation at a time: parallelism and concurrency. Our programs’ logic, however, is written in a mostly linear fashion. We’d like to be able to specify the operations a program should perform and points at which a function could pause and some other part of the program could run instead, without needing to specify up front exactly the order and manner in which each bit of code should run. Asynchronous programming is an abstraction that lets us express our code in terms of potential pausing points and eventual results that takes care of the details of coordination for us.

本章在第 16 章使用线程实现并行和并发的基础上,引入了另一种编写代码的方法:Rust 的 future、stream 以及 asyncawait 语法,它们让我们能够表达操作如何可以是异步的,以及实现异步运行时的第三方 crate:管理和协调异步操作执行的代码。

This chapter builds on Chapter 16’s use of threads for parallelism and concurrency by introducing an alternative approach to writing code: Rust’s futures, streams, and the async and await syntax that let us express how operations could be asynchronous, and the third-party crates that implement asynchronous runtimes: code that manages and coordinates the execution of asynchronous operations.

让我们考虑一个例子。假设你正在导出一段你为家庭庆祝活动创建的视频,这个操作可能需要几分钟到几小时不等。视频导出将尽可能多地使用 CPU 和 GPU 功率。如果你只有一个 CPU 核心,并且你的操作系统在导出完成之前没有暂停该操作——也就是说,如果它“同步 (synchronously)”地执行导出——那么在该任务运行时,你无法在计算机上做任何其他事情。那将是一个相当令人沮丧的体验。幸运的是,你计算机的操作系统可以而且确实经常无形地中断导出,以便让你能够同时完成其他工作。

Let’s consider an example. Say you’re exporting a video you’ve created of a family celebration, an operation that could take anywhere from minutes to hours. The video export will use as much CPU and GPU power as it can. If you had only one CPU core and your operating system didn’t pause that export until it completed—that is, if it executed the export synchronously—you couldn’t do anything else on your computer while that task was running. That would be a pretty frustrating experience. Fortunately, your computer’s operating system can, and does, invisibly interrupt the export often enough to let you get other work done simultaneously.

现在假设你正在下载别人分享的视频,这可能也需要一段时间,但不会占用那么多的 CPU 时间。在这种情况下,CPU 必须等待数据从网络到达。虽然你可以在数据开始到达时就开始读取数据,但可能需要一些时间才能让所有数据都出现。即使数据全部到齐了,如果视频很大,加载完它也可能至少需要一两秒钟。这听起来可能不算多,但对于现代处理器来说,这是一段非常长的时间,因为它每秒可以执行数十亿次操作。同样,你的操作系统会无形地中断你的程序,以便允许 CPU 在等待网络调用完成时执行其他工作。

Now say you’re downloading a video shared by someone else, which can also take a while but does not take up as much CPU time. In this case, the CPU has to wait for data to arrive from the network. While you can start reading the data once it starts to arrive, it might take some time for all of it to show up. Even once the data is all present, if the video is quite large, it could take at least a second or two to load it all. That might not sound like much, but it’s a very long time for a modern processor, which can perform billions of operations every second. Again, your operating system will invisibly interrupt your program to allow the CPU to perform other work while waiting for the network call to finish.

视频导出是“CPU 密集型 (CPU-bound)”或“计算密集型 (compute-bound)”操作的一个例子。它受限于计算机在 CPU 或 GPU 内的潜在数据处理速度,以及它可以为该操作投入多少速度。视频下载是“I/O 密集型 (I/O-bound)”操作的一个例子,因为它受限于计算机“输入和输出”的速度;它只能和数据通过网络发送的速度一样快。

The video export is an example of a CPU-bound or compute-bound operation. It’s limited by the computer’s potential data processing speed within the CPU or GPU, and how much of that speed it can dedicate to the operation. The video download is an example of an I/O-bound operation, because it’s limited by the speed of the computer’s input and output; it can only go as fast as the data can be sent across the network.

在这两个例子中,操作系统的无形中断提供了一种并发形式。不过,这种并发仅发生在整个程序的层面上:操作系统中断一个程序以让其他程序完成工作。在许多情况下,因为我们对程序的理解比操作系统要细致得多,所以我们可以发现操作系统看不见的并发机会。

In both of these examples, the operating system’s invisible interrupts provide a form of concurrency. That concurrency happens only at the level of the entire program, though: the operating system interrupts one program to let other programs get work done. In many cases, because we understand our programs at a much more granular level than the operating system does, we can spot opportunities for concurrency that the operating system can’t see.

例如,如果我们正在构建一个管理文件下载的工具,我们应该能够编写我们的程序,使得启动一个下载不会锁定 UI,并且用户应该能够同时开始多个下载。不过,许多用于与网络交互的操作系统 API 是“阻塞 (blocking)”的;也就是说,它们会阻塞程序的进度,直到它们正在处理的数据完全就绪。

For example, if we’re building a tool to manage file downloads, we should be able to write our program so that starting one download won’t lock up the UI, and users should be able to start multiple downloads at the same time. Many operating system APIs for interacting with the network are blocking, though; that is, they block the program’s progress until the data they’re processing is completely ready.

注意:如果你仔细想想,这就是“大多数”函数调用的工作方式。然而,“阻塞”一词通常保留给与文件、网络或计算机上的其他资源交互的函数调用,因为在这些情况下,单个程序会受益于操作是“非”阻塞的。

Note: This is how most function calls work, if you think about it. However, the term blocking is usually reserved for function calls that interact with files, the network, or other resources on the computer, because those are the cases where an individual program would benefit from the operation being non-blocking.

我们可以通过为下载每个文件启动一个专用线程来避免阻塞我们的主线程。然而,这些线程所使用的系统资源的开销最终会成为一个问题。最好是调用最初就不阻塞,相反我们可以定义一些我们希望程序完成的任务,并允许运行时选择运行它们的最佳顺序和方式。

We could avoid blocking our main thread by spawning a dedicated thread to download each file. However, the overhead of the system resources used by those threads would eventually become a problem. It would be preferable if the call didn’t block in the first place, and instead we could define a number of tasks that we’d like our program to complete and allow the runtime to choose the best order and manner in which to run them.

这正是 Rust 的 “async” (asynchronous 的缩写) 抽象带给我们的。在本章中,你将通过以下主题全面了解 async:

  • 如何使用 Rust 的 asyncawait 语法并通过运行时执行异步函数
  • 如何使用异步模型解决我们在第 16 章中看到的一些相同挑战
  • 多线程和异步如何提供互补的解决方案,在许多情况下你可以将它们结合起来

That is exactly what Rust’s async (short for asynchronous) abstraction gives us. In this chapter, you’ll learn all about async as we cover the following topics:

  • How to use Rust’s async and await syntax and execute asynchronous functions with a runtime
  • How to use the async model to solve some of the same challenges we looked at in Chapter 16
  • How multithreading and async provide complementary solutions that you can combine in many cases

不过,在看异步在实践中如何工作之前,我们需要稍作绕道,讨论一下并行和并发之间的区别。

Before we see how async works in practice, though, we need to take a short detour to discuss the differences between parallelism and concurrency.

并行与并发 (Parallelism and Concurrency)

Parallelism and Concurrency

到目前为止,我们大多将并行和并发视为可以互换的。现在我们需要更精确地对它们进行区分,因为随着我们开始工作,差异就会显现出来。

We’ve treated parallelism and concurrency as mostly interchangeable so far. Now we need to distinguish between them more precisely, because the differences will show up as we start working.

考虑一个团队拆分软件项目工作的不同方式。你可以给单个人员分配多个任务,给每个成员分配一个任务,或者混合使用这两种方法。

Consider the different ways a team could split up work on a software project. You could assign a single member multiple tasks, assign each member one task, or use a mix of the two approaches.

当一个人在完成任何任务之前处理几个不同的任务时,这就是“并发 (concurrency)”。实现并发的一种方法类似于在你的电脑上签出了两个不同的项目,当你对一个项目感到无聊或卡住时,你就切换到另一个项目。你只有一个人,所以你无法在完全相同的时间在两个任务上取得进展,但你可以多任务处理,通过在它们之间切换一次在一个任务上取得进展(见图 17-1)。

When an individual works on several different tasks before any of them is complete, this is concurrency. One way to implement concurrency is similar to having two different projects checked out on your computer, and when you get bored or stuck on one project, you switch to the other. You’re just one person, so you can’t make progress on both tasks at the exact same time, but you can multitask, making progress on one at a time by switching between them (see Figure 17-1).

显示了标记为任务 A 和任务 B 的叠加方框,方框中有代表子任务的菱形。箭头从 A1 指向 B1,从 B1 指向 A2,从 A2 指向 B2,从 B2 指向 A3,从 A3 指向 A4,从 A4 指向 B3。子任务之间的箭头穿过了任务 A 和任务 B 之间的方框。
图 17-1:一个并发工作流,在任务 A 和任务 B 之间切换

当团队通过让每个成员承担一项任务并独立完成来拆分一组任务时,这就是“并行 (parallelism)”。团队中的每个人都可以在完全相同的时间取得进展(见图 17-2)。

When the team splits up a group of tasks by having each member take one task and work on it alone, this is parallelism. Each person on the team can make progress at the exact same time (see Figure 17-2).

显示了标记为任务 A 和任务 B 的叠加方框,方框中有代表子任务的菱形。箭头从 A1 指向 A2,从 A2 指向 A3,从 A3 指向 A4,从 B1 指向 B2,从 B2 指向 B3。任务 A 和任务 B 的方框之间没有穿过的箭头。
图 17-2:一个并行工作流,工作在任务 A 和任务 B 上独立进行

在这两种工作流中,你可能都需要在不同任务之间进行协调。也许你认为分配给一个人的任务完全独立于其他人的工作,但它实际上需要团队中的另一个人先完成他们的任务。部分工作可以并行完成,但其中一部分实际上是“串行 (serial)”的:它只能以序列的方式发生,一个任务接一个任务,如图 17-3 所示。

In both of these workflows, you might have to coordinate between different tasks. Maybe you thought the task assigned to one person was totally independent from everyone else’s work, but it actually requires another person on the team to finish their task first. Some of the work could be done in parallel, but some of it was actually serial: it could only happen in a series, one task after the other, as in Figure 17-3.

显示了标记为任务 A 和任务 B 的叠加方框,方框中有代表子任务的菱形。在任务 A 中,箭头从 A1 指向 A2,从 A2 指向一对像“暂停”符号的粗垂直线,再从该符号指向 A3。在任务 B 中,箭头从 B1 指向 B2,从 B2 指向 B3,从 B3 指向 A3,以及从 B3 指向 B4。
图 17-3:一个部分并行的工作流,其中任务 A 和任务 B 独立进行,直到任务 A3 被阻塞在任务 B3 的结果上。

同样,你可能会意识到你自己的一个任务依赖于你的另一个任务。现在你的并发工作也变成了串行的。

Likewise, you might realize that one of your own tasks depends on another of your tasks. Now your concurrent work has also become serial.

并行和并发也可以相互交织。如果你得知一个同事在等你完成你的一个任务之前一直被卡住,你可能会把所有精力都集中在那个任务上以“解除”你同事的阻塞。你和你的同事不再能够并行工作,你也无法再在自己的任务上并发工作。

Parallelism and concurrency can intersect with each other, too. If you learn that a colleague is stuck until you finish one of your tasks, you’ll probably focus all your efforts on that task to “unblock” your colleague. You and your coworker are no longer able to work in parallel, and you’re also no longer able to work concurrently on your own tasks.

同样的动态也发生在软件和硬件上。在只有单个 CPU 核心的机器上,CPU 一次只能执行一个操作,但它仍然可以并发工作。利用线程、进程和异步等工具,计算机可以暂停一项活动并在最终循环回到第一项活动之前切换到其他活动。在具有多个 CPU 核心的机器上,它还可以并行工作。一个核心可以执行一个任务,而另一个核心执行一个完全不相关的任务,这些操作实际上是同时发生的。

The same basic dynamics come into play with software and hardware. On a machine with a single CPU core, the CPU can perform only one operation at a time, but it can still work concurrently. Using tools such as threads, processes, and async, the computer can pause one activity and switch to others before eventually cycling back to that first activity again. On a machine with multiple CPU cores, it can also do work in parallel. One core can be performing one task while another core performs a completely unrelated one, and those operations actually happen at the same time.

在 Rust 中运行异步代码通常是并发发生的。取决于硬件、操作系统以及我们使用的异步运行时(稍后会有更多关于异步运行时的介绍),这种并发在底层也可能使用并行。

Running async code in Rust usually happens concurrently. Depending on the hardware, the operating system, and the async runtime we are using (more on async runtimes shortly), that concurrency may also use parallelism under the hood.

现在,让我们深入了解 Rust 中的异步编程究竟是如何工作的。

Now, let’s dive into how async programming in Rust actually works.