Compare commits

...

3 Commits

Author SHA1 Message Date
KaiserY
accd8b6537 proofreading ch17-00 2024-10-22 00:39:07 +08:00
KaiserY
ec1d865a68 update ch17 2024-10-22 00:01:15 +08:00
KaiserY
d511daa787 update ch17 2024-10-21 17:55:39 +08:00
4 changed files with 124 additions and 16 deletions

View File

@ -38,7 +38,9 @@ $ cd hello_world
现在打开刚创建的 *main.rs* 文件,输入示例 1-1 中的代码。
<Listing number="1-1" file-name="main.rs" caption="一个打印 `Hello, world!` 的程序">
<figure class="listing">
<span class="file-name">文件名main.rs</span>
```rust
fn main() {
@ -46,7 +48,10 @@ fn main() {
}
```
</Listing>
<figcaption>示例 1-1一个打印 `Hello, world!` 的程序</figcaption>
</figure>
保存文件,并回到当前目录为“~/projects/hello_world”的终端窗口。在 Linux 或 macOS 上,输入如下命令,编译并运行文件:

View File

@ -35,7 +35,9 @@ $ cd hello_cargo
请自行选用文本编辑器打开 *Cargo.toml* 文件。它应该看起来如示例 1-2 所示:
<Listing number="1-2" file-name="Cargo.toml" caption="*cargo new* 命令生成的 *Cargo.toml* 的内容">
<figure class="listing">
<span class="file-name">文件名Cargo.toml</span>
```toml
[package]
@ -48,7 +50,9 @@ edition = "2021"
[dependencies]
```
</Listing>
<figcaption>示例 1-2*cargo new* 命令生成的 *Cargo.toml* 的内容</figcaption>
</figure>
这个文件使用 [*TOML*][toml]<!-- ignore --> (*Tom's Obvious, Minimal Language*) 格式,这是 Cargo 配置文件的格式。

View File

@ -49,13 +49,17 @@ $ cd guessing_game
猜数字程序的第一部分请求和处理用户输入,并检查输入是否符合预期的格式。首先,我们会允许玩家输入一个猜测。在 _src/main.rs_ 中输入示例 2-1 中的代码。
<Listing number="2-1" file-name="src/main.rs" caption="获取用户猜测并打印的代码">
<figure class="listing">
<span class="file-name">文件名src/main.rs</span>
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:all}}
```
</Listing>
<figcaption>示例 2-1获取用户猜测并打印的代码</figcaption>
</figure>
这些代码包含很多信息,我们一行一行地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 `io` 输入/输出库引入当前作用域。`io` 库来自于标准库,也被称为 `std`
@ -219,7 +223,7 @@ Cargo 认为这些版本与 `0.8.5` 版本的公有 API 相兼容,这样的版
现在,不修改任何代码,构建项目,如示例 2-2 所示。
<Listing number="2-2" caption="将 rand crate 添加为依赖之后运行 `cargo build` 的输出">
<figure class="listing">
```console
$ cargo build
@ -242,7 +246,9 @@ $ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
```
</Listing>
<figcaption>示例 2-2将 rand crate 添加为依赖之后运行 `cargo build` 的输出</figcaption>
</figure>
可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),并且显示的行数可能会有所不同(取决于操作系统),行的顺序也可能会不同。
@ -293,13 +299,17 @@ rand = "0.9.0"
让我们开始使用 `rand` 来生成一个猜数字随机数。下一步是更新 *src/main.rs*,如示例 2-3 所示。
<Listing number="2-3" file-name="src/main.rs" caption="添加生成随机数的代码">
<figure class="listing">
<span class="file-name">文件名src/main.rs</span>
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-03/src/main.rs:all}}
```
</Listing>
<figcaption>示例 2-3添加生成随机数的代码</figcaption>
</figure>
首先,我们新增了一行 `use rand::Rng;`。`Rng` 是一个 trait它定义了随机数生成器应实现的方法想使用这些方法的话此 trait 必须在作用域中。第十章会详细介绍 trait。
@ -338,13 +348,17 @@ You guessed: 5
现在有了用户输入和一个随机数,我们可以比较它们。这个步骤如示例 2-4 所示。注意这段代码还不能通过编译,我们稍后会解释。
<Listing number="2-4" file-name="src/main.rs" caption="处理比较两个数字可能的返回值">
<figure class="listing">
<span class="file-name">文件名src/main.rs</span>
```rust,ignore,does_not_compile
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-04/src/main.rs:here}}
```
</Listing>
<figcaption>示例 2-4处理比较两个数字可能的返回值</figcaption>
</figure>
首先我们增加了另一个 `use` 声明,从标准库引入了一个叫做 `std::cmp::Ordering` 的类型到作用域中。 `Ordering` 也是一个枚举,不过它的成员是 `Less`、`Greater` 和 `Equal`。这是比较两个值时可能出现的三种结果。
@ -464,13 +478,17 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
为了进一步改善游戏性,不要在用户输入非数字时崩溃,需要忽略非数字,让用户可以继续猜测。可以通过修改 `guess``String` 转化为 `u32` 那部分代码来实现,如示例 2-5 所示:
<Listing number="2-5" file-name="src/main.rs" caption="忽略非数字的猜测并重新请求数字而不是让程序崩溃">
<figure class="listing">
<span class="file-name">文件名src/main.rs</span>
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-05/src/main.rs:here}}
```
</Listing>
<figcaption>示例 2-5忽略非数字的猜测并重新请求数字而不是让程序崩溃</figcaption>
</figure>
我们将 `expect` 调用换成 `match` 语句,以从遇到错误就崩溃转换为处理错误。须知 `parse` 返回一个 `Result` 类型,而 `Result` 是一个拥有 `Ok``Err` 成员的枚举。这里使用的 `match` 表达式,和之前处理 `cmp` 方法返回 `Ordering` 时用的一样。
@ -505,13 +523,17 @@ You win!
太棒了!再有最后一个小的修改,就能完成猜数字游戏了:还记得程序依然会打印出秘密数字。在测试时还好,但正式发布时会毁了游戏。删掉打印秘密数字的 `println!`。示例 2-6 为最终代码:
<Listing number="2-6" file-name="src/main.rs" caption="猜数字游戏的完整代码">
<figure class="listing">
<span class="file-name">文件名src/main.rs</span>
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-06/src/main.rs}}
```
</Listing>
<figcaption>示例 2-6猜数字游戏的完整代码</figcaption>
</figure>
此时此刻,你顺利完成了猜数字游戏。恭喜!

View File

@ -1 +1,78 @@
# Async 和 await
> [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”操作因为其受限于计算机的 *输入输出* 速度。下载的速度最多只能与通过网络传输数据的速度一致。
在上述两个例子中,操作系统的隐式中断提供了一种形式的并发。不过这种并发仅限于整个程序的级别:操作系统中断一个程序并让其它程序得以执行。在很多场景中,由于我们能比操作系统在更细粒度上理解我们的程序,因此我们可以观察到很多操作系统无法察觉的并发机会。
例如,如果我们在构建一个管理文件下载的工具,我们应当以一种不会因开始一个下载任务而锁定 UI 的方式来编写程序,并且用户应该能够同时开始多个下载任务。不过很多操作系统与网络交互的 API 都是 *阻塞* 的(*blocking*)。也就是说这些 API 会阻塞程序的进程,直到它们处理的数据完全就绪。
> 注意:如果你仔细思索一下,会发现这是 *大部分* 函数调用的工作方式!不过我们通常将 “阻塞” 这个术语保留给那些与文件、网络或其它计算机资源交互的函数调用,因为这些地方是单个程序可以从 *非* 阻塞操作中获益的地方。
我们可以新建专用的线程来下载每个文件以免阻塞主线程。然而,我们最终会发现这些线程的开销会成为一个问题。如果这些调用在一开始就是非阻塞的话那就更理想了。最后,如果我们能够像在阻塞代码中一样,以直接的风格编写非阻塞代码,那就更好了。比如这样:
```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>
<img alt="并发工作流" src="img/trpl17-02.svg" class="center" />
<figcaption>图 17-2一个并行流其中任务 A 和任务 B 的工作同时独立进行</figcaption>
</figure>
在这两种场景中,你可能需要协调不同的任务。也许你 *认为* 某个人负责的任务与其他人的工作完全不相关,但实际上它确实依赖于团队中另一位成员的工作完成。一些工作可以并行进行,不过一些工作事实上是 *串行* 的:它们只能串行地发生,一个接着一个,如图 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` 语法
- 探索如何使用异步模型来解决第十六章中遇到的一些挑战
- 了解多线程和异步如何互补,在很多场景中你甚至可以同时使用两者