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