Updated 'src/async/futures.md'.

This commit is contained in:
Hector PENG
2026-04-09 10:49:18 +08:00
parent 62cad9f40b
commit 798fc05c8f
3 changed files with 76 additions and 82 deletions

View File

@@ -1,19 +1,34 @@
use trpl::Html;
use trpl::{Either, Html};
async fn main() {
fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("{url} 的标题是 {title}"),
None => println!("{url} 没有标题"),
}
let fut_result = async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::select(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println! ("{url} 先返回");
match maybe_title {
Some(title) => println! ("其页面标题为: '{title}'"),
None => println! ("他没有标题。"),
}
};
trpl::block_on(fut_result)
}
async fn page_title(url: &str) -> Option<String> {
let response = trpl::get(url).await.text().await;
Html::parse(&response)
async fn page_title(url: &str) -> (&str, Option<String>) {
let response_text = trpl::get(url).await.text().await;
let title = Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
.map(|title| title.inner_html());
(url, title)
}

View File

@@ -42,7 +42,7 @@ enum Poll<T> {
当咱们看到使用了 `await` 的代码时Rust 会在背后将其编译为调用 `poll` 的代码。回顾 [清单 17-4](futures.md#listing-17-4),其中我们曾打印出单个 URL 解析后的页面标题Rust 就会将其编译成类似(但不完全)下面这样的代码:
当咱们看到使用了 `await` 的代码时Rust 会在背后将其编译为调用 `poll` 的代码。回顾 [清单 17-4](futures.md#listing_17-4),其中我们曾打印出单个 URL 解析后的页面标题Rust 就会将其编译成类似(但不完全)下面这样的代码:
```rust

View File

@@ -13,7 +13,7 @@ Rust 中异步编程的关键要素是 *未来值* 和 Rust 的 `async` 与 `awa
这可能感觉有点抽象,所以我们来编写第一个异步程序:一个小的 web 爬虫。我们将从命令行传入两个 URL并发地获取这两个 URL并返回先完成的那个的结果。这个示例将有不少新语法但不用担心 -- 我们将在过程中解释咱们需要了解的一切。
## 我们的第一个异步程序
# 我们的第一个异步程序
为了将这一章的重点放在学习异步而不是处理该生态的各个部分上,我们创建了 `trpl` 代码箱(`trpl` 是 “The Rust Programming Language” 的缩写)。他重导出了咱们需要的所有类型、特质及函数,他们主要来自 `futures``tokio` 代码箱。`futures` 代码箱是 Rust 异步代码实验的正式场所,并且他实际上是 `Future` 特质的最初设计地。[Tokio](https://tokio.rs/) 是如今 Rust 中使用最广泛的异步运行时,特别是对于 web 应用。当然还有其他很棒的运行时,而他们可能更适合咱们的目的。我们之所以在 `trpl` 的底层使用 `tokio` 代码箱,是因为他经过充分测试且被广泛使用。
@@ -32,7 +32,7 @@ cargo add trpl
现在我们可以使用 `trpl` 提供的各个部分来编写我们的第一个异步程序了。我们将构建出一个小型命令行工具,他将获取两个 web 页面,提取每个页面中的 `<title>` 元素,并打印出首先完成整个过程的页面的标题。
### 定义 `page_title` 函数
## 定义 `page_title` 函数
我们以编写一个函数开始,他将取一个页面的 URL 作为参数,向其发出请求,并返回 `<title>` 元素的文本(见清单 17-1
@@ -110,7 +110,7 @@ fn page_title(url: &str) -> impl Future<Output = Option<String>> {
现在我们可以在 `main` 中调用 `page_title`
### 通过运行时执行异步函数
## 通过运行时执行异步函数
首先,我们将获取单个页面的标题,如下清单 17-3 中所示。不幸的是,这段代码还不会编译。
@@ -142,54 +142,46 @@ error[E0752]: `main` function is not allowed to be `async`
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
```
`main` 不能标记为 `async` 的原因,是异步代码需要一个 *运行时*一个管理执行异步代码细节的 Rust 代码箱。某个程序的 `main` 函数可以 *初始化* 某个运行时,但 *他本身* 并不是运行时。(我们将更多地了解为什么会这种情况。)每个执行异步代码的 Rust 程序,都至少有一个其设置了某个运行时,并执行未来值之处
`main` 不能标记为 `async` 的原因,是异步代码需要 *运行时*:管理执行异步代码细节的某个 Rust 代码箱。程序的 `main` 函数可以 *初始化* 运行时,但 *他本身* 并不是运行时。(稍后我们将详细了解为什么会出现这种情况。)每个执行异步代码的 Rust 程序,都至少有一处,设置执行未来值的运行时的地方
大多数支持异步的语言都捆绑了运行时,但 Rust 没有。相反,存在许多可用的不同异步运行时,每种都针对其目标用例做出了不同的恰当取舍。例如,有着众多 CPU 核心及大量 RAM 的高吞吐量 web 服务器,与仅有单个核心、少量 RAM 且不具备堆分配能力的微控制器 MCU 相比,就有着截然不同的需求。提供这些运行时的代码箱,通常还会提供常用功能的异步版本,比如文件或网络 I/O 等。
支持异步的大多数语言,都捆绑了某个运行时,但 Rust 并没有。相反,有许多不同异步运行时可用,每一种都根据其针对的用例,做出了不同取舍。例如,有着众多 CPU 核心及大量 RAM 的高吞吐量 web 服务器,与仅有一个核心、少量 RAM 且不具备内存堆分配能力的微控制器,就有着截然不同的需求。提供这些运行时的代码箱,通常还提供了诸如文件或网络 I/O 等常用功能的异步版本
在这里,以及在本章的其余部分,我们将使用 `trpl` 代码中的 `run` 函数,他会取一个未来值作为参数,并将其运行完成。在幕后,调用 `run` 会设置一个用于运行传入未来值的运行时。一旦该未来值运行完成,`run` 就会返回该未来值所产生的任何值。
我们可将由 `page_title` 返回的未来值,直接传递给 `run`,一旦其运行完成,我们就可以匹配得到的 `Option<String>`,就像咱们在清单 17-3 中所尝试的那样。但是,在本章的大多数示例中(以及现实世界中大多数异步代码中),我们将执行不止一个异步函数调用,因此我们将传递一个 `async` 代码块,并显式等待 `page_title` 调用的结果,如清单 17-4 所示。
在这里,以及本章的其余部分中,我们将使用 `trpl` 代码中的 `block_on` 函数,他会取一个未来值作为参数,并阻塞当前线程,知道该未来值运行完成。在幕后,调用 `block_on` 会使用 `tokio` 设置运行时,用于运行传入的未来值(`trpl` 代码箱的 `block_on` 的行为类似于其他代码箱的 `block_on` 函数)。一旦该未来值完成,`block_on` 就会返回该未来值产生的任何值
我们可以直接传递 `page_title` 返回的未来值给 `block_on`,并在其完成后,就可以像在清单 17-3 中尝试的那样,匹配得到的 `Option<String>`。但是,对于这一章中的大多数示例中(以及现实世界中的大多数异步代码),我们都将执行不止一个异步函数调用,因此我们将传递一个 `async` 代码块,并显式等待 `page_title` 调用的结果,如下清单 17-4 中所示。
<a name="listing_17-4"></a>
文件名:`src/main.rs`
```rust
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
trpl::block_on(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
Some(title) => println!("{url} 的标题是 {title}"),
None => println!("{url} 没有标题"),
}
})
}
```
**清单 17-4**:通过 `trpl:block_on` 等待异步代码块
<a name="listing-17-4"></a> *清单 17-4使用 `trpl:run` 等待某个异步代码块*
当我们运行这段代码时,我们会得到最初咱们预期的行为:
当我们运行这段代码时,我们得到了最初预期的行为:
```console
$ cargo run -- https://news.163.com
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/hello-async 'https://news.163.com'`
The title for https://news.163.com was 网易新闻
$ cargo run -- https://rust-lang.xfoss.com
Compiling hello-async v0.1.0 (/home/hector/rust-lang-zh_CN/projects/hello-async)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/hello-async 'https://rust-lang.xfoss.com'`
https://rust-lang.xfoss.com 的标题是 序和前言 - Yet another Chinese rust-lang book.
```
呼--我们终于有了一些可以工作的异步代码!不过,但在添加代码让两个网站互相竞赛之前,我们先来简要回顾一下未来值的工作原理。
呼 -- 我们终于有了一些可以工作的异步代码!不过,在我们添加让两个网站互相竞赛的代码前,我们来简单回顾一下,这些未来值的工作原理。
每个 *等待点await point* -- 即代码用到 `await` 关键字的各处 -- 都表示了一个控制权被交还给运行时之处。为实现这一点Rust 需要跟踪涉及到异步代码块的状态以便运行时可以启动其他工作然后在准备好再次尝试推进第一个工作时再返回。这是个不可见的状态机an invisible state machine就像咱们写了个保存每个等待点当前状态的枚举一样
每个 *等待点await point* -- 也就是代码中使用 `await` 关键字的每处 -- 都代表控制权交还给运行时的位置。为了做到这一点Rust 需要跟踪异步代码块中涉及的状态以便运行时可以启动一些其他工作然后在准备好再次尝试推进第一项工作时返回。这属于不可见隐形的状态机an invisible state machine就好像咱们编写了个如下的枚举来保存每个等待点处的当前状态一样
```rust
@@ -200,77 +192,67 @@ enum PageTitleFuture<'a> {
}
```
然而,亲自编写在各个状态间转换的代码,既繁琐又容易出错,尤其是咱们需要随后添加更多功能及更多状态时。幸运的是Rust 编译器会自动创建管理异步代码的状态机数据结构。有关数据结构的正常借用和所有权规则仍然适用,同时令人高兴的是,编译器还会为我们检查这些规则,并提供有用的错误消息。我们将在本章稍后部分讨论这些问题
然而,手动编写代码以在各个状态间转换既繁琐又容易出错,尤其是在日后咱们需要添加更多功能状态时。幸运的是Rust 编译器会自动创建管理异步代码的状态机数据结构。围绕数据结构的正常借用和所有权规则仍然适用,并且令人高兴的是,编译器还会为我们检查这些检查,并提供有用的错误消息。我们将在这一章后面讨论其中一些方面
最终,必须有某种机制来执行这个状态机,而这个机制就是运行时(这就是为什么咱们在研究运行时时,可能会遇到 *执行器executors* 的提法:所谓执行器,是运行时中负责执行异步代码的部分。)
现在咱们就能明白,为什么在清单 17-3 中,编译器阻止我们构造 `main` 本身为一个异步函数。若 `main` 是个异步函数,针对`main` 返回的未来值,就需要由其他组件来管理状态机,但 `main` 是程序的起点!因此,我们在 `main` 中调用 `trpl::block_on` 函数来设置运行时,并运行 `async` 代码块返回的未来值,知道其完成。
> **注意**:某些运行提供了宏,以便咱们 *可以* 编写异步的 `main` 函数。这些宏会重写 `async fn main() { ... }` 为普通的 `fn main`,这与我们在清单 17-4 中手动完成的相同:调用一个函数,该函数会像 `trpl::run` 那样运行未来值至完成。
现在,我们来把这部分放在一起,看看我们可以怎样编写并发代码。
最终,必须有某种东西来执行这个状态机,而这个东西就是运行时(这就是为什么在研究运行时时,可能会遇到 *执行器executors* 的概念:所谓执行器,是某个运行时中负责执行异步代码的部分。)
现在咱们就明白,为什么编译器阻止了我们在清单 17-3 中,将 `main` 本身作为一个异步函数了吧。如果 `main` 是个异步函数,那么无论 `main` 返回什么样的未来值,都需要其他东西来管理状态机,但 `main` 是程序的起点!相反,我们在 `main` 中调用了 `trpl::run` 函数设置运行时,并在那个 `async` 返回 `Ready` 时,运行由其返回的未来值。
> **注意**:有些运行时提供以便咱们编写异步 `main` 函数的宏。这些宏会重写 `async fn main() { ... }` 为普通的 `fn main`,这与我们在清单 17-5 中,手工编写的相同:调用某个像 `trpl::run` 那样,运行某个未来值至完成。
现在,我们来将这些代码片段组合在一起,看看咱们如何编写并发代码。
### 让我们的两个 URL 相互竞赛
在下面的清单 17-5 中,我们以命令行上传入的两个不同 URL 调用 `page_title`,并进行比赛。
## 让两个 URL 并发地相互竞争
在下面的清单 17-5 中,我们从命令行以两个传入的两个不同 URL 调用 `page_title`,并通过选择先完成的未来值让他们竞争。
<a name="listing_17-5"></a>
文件名:`src/main.rs`
```rust
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let fut_result = async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::race(title_fut_1, title_fut_2).await {
match trpl::select(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} returned first");
println! ("{url} 先返回");
match maybe_title {
Some(title) => println!("Its page title is: '{title}'"),
None => println!("Its title could not be parsed."),
Some(title) => println! ("其页面标题为: '{title}'"),
None => println! ("他没有标题。"),
}
})
};
trpl::block_on(fut_result)
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let text = trpl::get(url).await.text().await;
let title = Html::parse(&text)
let response_text = trpl::get(url).await.text().await;
let title = Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
```
**清单 17-5**:对两个 URL 调用 `page_title` 以查看那个先返回
*清单 17-5*
我们以对用户提供的每个 URL 调用 `page_title` 开始。我们保存得到的未来值为 `title_fut_1``title_fut_2`。请记住,这些还不会执行任何操作,因为未来值是懒惰的,且我们尚未对其进行等待。然后我们传递未来值给 `trpl::select`,他会返回一个值,表明传递给他的未来值中哪个先完成。
我们以对用户提供的每个 URL调用 `page_title` 开始。我们将得到的未来值,保存为 `title_fut_1``title_fut_2`。请记住,这些未来值还不会做任何事情,因为未来值是懒惰的,且我们还没有等待他们。然后,我们将这两个未来值传递给 `trpl::race`,他会返回一个表明传递给他的未来值中,首先完成那个的值
> **注意**:在底层,`trpl::select` 是基于定义在 `futures` 代码箱中的一个更通用的 `select` 函数构建的。`futures` 代码箱的 `select` 函数可以完成 `trpl::select` 函数无法完成的许多事情,但他也有一些额外的复杂性,目前我们可以跳过
> **注意**:表象之下,`race` 是建立在一个更通用的函数 `select` 基础上的,在真实世界 Rust 代码中,咱们将更经常遇到。`select` 函数可以完成很多 `trpl::race` 函数无法完成的事情,但他有一些我们现在可以跳过的额外复杂度。
两个未来值都可以合法地 “获胜”,因此返回 `Result` 是没有意义的。取而代之的是,`race` 会返回一种我们以前从未见过的类型 `trpl::Either``Either` 类型有点类似于 `Result`,因为他有两种情况。但与 `Result` 不同的是,`Either` 中没有成功或失败的概念。相反,他使用了 `Left``Right`,表示 “非此即彼”:
由于两个未来值都可以合法地 “胜出”,因此返回 `Result` 并不合理。相反,`trpl::select` 返回一种我们以前从未见过的类型 `trpl::Either``Either` 类型与 `Result` 有点相似,因为他有两种情况。但与 `Result` 不同的是,`Either` 中没有成功或失败的概念。相反,他使用 `Left``Right` 来表示 “二者之一”:
```Rust
@@ -280,14 +262,11 @@ enum Either<A, B> {
}
```
当第一个参数获胜时,`select` 函数返回带有该未来值的输出的 `Left`,当第二个未来值参数获胜时,则返回带有 ** 未来值的输出的 `Right`。这与调用该函数时参数出现的顺序一致:第一个参数位于第二个参数的左侧。
在第一个参数获胜时,则 `run` 函数就会返回 `Left`,以及该未来值的输出结果;在第二个未来值参数获胜时,则返回 `Right`,以及该参数的输出结果。这与调用该函数时,参数出现的顺序一致:第一个参数是在第二个参数的左边
我们还更新了 `page_title` 为返回所传入的同一 URL。这样当首先返回的页面没有我们可以解析的 `<title>` 时,我们仍然可以打印出一条有意义的消息。有了这些信息,我们最后更新 `println!` 的输出为既表明哪个 URL 先完成,也表明该 URL 处的 web 页面的 `<title>`,在有该元素时
我们还更新了 `page_title`,使其返回所传入的同一 URL。这样在最先返回的页面没有我们可解析的 `<title>` 时,我们仍然可以打印出一条有意义的消息。有了这些信息,我们就可以通过更新 `println!` 的输出,表明哪个 URL 最先完成,并在该 URL 的网页有 `<title>` 元素时,该元素为何。
现在,咱们已经构建了一个可工作的小型 web 爬虫!请选择几个 URL 并运行这个命令行工具。咱们可能会发现,一些网站的速度始终比其他网站快,而在其他情况下,速度较快的网站每次运行会有所不同。更重要的是,咱们已经掌握了使用未来值的基础知识,现在我们可以更深入研究,使用异步咱们可以做些什么了。
现在,咱们已经构建了个可运行的小型 web 爬虫!请选择几个 URL 并运行这个命令行工具。咱们可能会发现,一些网站始终比其他网站快,而在其他情况下,较快的网站每次运行会有所不同。更重要的是,咱们已经掌握了使用未来值的基础知识,因此现在我们可以更深入地探索在异步下我们可以做些什么。
End