Updated 'src/async/multiple_futures.md'.

This commit is contained in:
Hector PENG
2026-04-11 18:56:56 +08:00
parent d50c9be81c
commit 3240c5d08c
6 changed files with 257 additions and 5 deletions

View File

@@ -0,0 +1,7 @@
[package]
name = "custom_async_abstraction"
version = "0.1.0"
edition = "2024"
[dependencies]
trpl = "0.3.0"

View File

@@ -0,0 +1,19 @@
use std::time::Duration;
fn main() {
let fut = async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"最终完成"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("在 '{message}' 下成功"),
Err(duration) => {
println!("{} 秒后失败", duration.as_secs())
}
}
};
trpl::block_on(fut);
}

View File

@@ -0,0 +1,7 @@
[package]
name = "starvation_demo"
version = "0.1.0"
edition = "2024"
[dependencies]
trpl = "0.3.0"

View File

@@ -0,0 +1,38 @@
use std::{thread, time::Duration};
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' 运行了 {ms}ms");
}
fn main() {
let fut = async {
let a = async {
println!("'a' 已启动。");
slow("a", 30);
trpl::yield_now().await;
slow("a", 10);
trpl::yield_now().await;
slow("a", 20);
trpl::yield_now().await;
println!("'a' 已结束。");
};
let b = async {
println!("'b' 已启动。");
slow("b", 75);
trpl::yield_now().await;
slow("b", 10);
trpl::yield_now().await;
slow("b", 15);
trpl::yield_now().await;
slow("b", 350);
trpl::yield_now().await;
println!("'b' 已结束。");
};
trpl::select(a, b).await;
};
trpl::block_on(fut);
}

View File

@@ -381,10 +381,10 @@ hi 来自第一个任务的数字 9 !
我们已经探讨了
- 如何使用消息传递,在未来值之间发送数据
- 异步代码块内的代码如何顺序执行
- 怎样迁移所有权到异步代码块中
- 以及怎样合并多个未来值
- 如何使用消息传递,在未来值之间发送数据
- 异步代码块内的代码如何顺序执行
- 怎样迁移所有权到异步代码块中
- 以及怎样合并多个未来值
接下来,我们来讨论怎样以及为何要告知运行时,他可以切换到另一个任务。

View File

@@ -1,7 +1,188 @@
# 使用任意数量的未来值
## 将控制权交换给运行时
在上一小节中,当我们从使用两个未来值,转换为使用三个未来值时,我们也不得不从使用 `join` 转换为使用 `join3`。如果每次改变我们要连接的未来值数量时,都要调用不同函数,那就太麻烦了。幸运的是,我们有个宏形式的 `join`,使用他我们就可以传递任意数量的参数。他还能自己处理等待未来值。因此,我们可以将清单 17-13 中的代码,重写为使用 `join!` 而非 `join3`,如下清单 17-14 中所示
回顾 [我们的第一个异步程序](./futures.md#我们的第一个异步程序)在每个等待点Rust 都会给予运行时一个机会来暂定任务,并在等待未来值尚未准备好时切换到另一任务。反之亦然Rust 在等待点处 ** 暂停异步代码块,并交还控制权给运行时。等待点之间的一切都是同步的
这意味着,当咱们在不带等待点的异步代码块中执行大量工作时,该未来值将阻塞任何其他未来值。咱们有时会听说这种情况被称为 “一个未来值 *饿死* 其他未来值”。在某些情况下,这可能无关紧要。然而,但咱们正在进行某种开销很高的设置或长时间运行的工作,或者有个未来值将无限期地执行某项特定任务时,咱们就需要考虑何时,以及在何处交还控制权给运行时。
我们来模拟一个长时间运行的操作,来演示 “饥饿” 问题,然后探讨怎样解决他。下面清单 17-14 引入了一个 `slow` 函数。
<a name="listing_17-14"></a>
```rust
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' 运行了 {ms}ms");
}
```
**清单 17-14**:使用 `thread::sleep` 模拟慢速操作
这段代码使用 `std::thread::sleep` 而不是 `trpl::sleep`,以便调用 `slow` 将阻塞当前线程数毫秒。我们可以使用 `slow` 代替现实世界中即长时间运行又具有阻塞性的操作。
在下面清单 17-15 中,我们使用 `slow` 来模拟在一对未来值中,执行此类 CPU 密集型的工作。
<a name="listing_17-15"></a>
```rust
let a = async {
println!("'a' 已启动。");
slow("a", 30);
slow("a", 10);
slow("a", 20);
trpl::sleep(Duration::from_millis(50)).await;
println!("'a' 已结束。");
};
let b = async {
println!("'b' 已启动。");
slow("b", 75);
slow("b", 10);
slow("b", 15);
slow("b", 350);
trpl::sleep(Duration::from_millis(50)).await;
println!("'b' 已结束。");
};
trpl::select(a, b).await;
```
**清单 17-15**:调用 `slow` 函数来模拟慢速操作
每个未来值都 ** 会在执行完一系列慢速操作后,交还控制权给运行时。当咱们运行这段代码时,将看到以下输出:
```console
'a' 已启动。
'a' 运行了 30ms
'a' 运行了 10ms
'a' 运行了 20ms
'b' 已启动。
'b' 运行了 75ms
'b' 运行了 10ms
'b' 运行了 15ms
'b' 运行了 350ms
'a' 已结束。
```
正如在 [清单 17-5](./futures.md#listing_17-5) 中,我们使用 `trpl::select` 让两个未来值在获取两个 URL 上竞争,`select` 仍会在 `a` 完成后立即结束。不过,这两个未来值中对 `slow` 的调用之间不存在交错执行。未来值 `a` 会执行他的所有工作,直到 `trpl::sleep` 调用被等待;随后未来值 `b` 执行所有工作,直到他自己的 `trpl::sleep` 调用被等待;最后未来值 `a` 完成。为了让两个未来值都能在他们的慢速任务之间取得进展,我们需要等待点,以便我们可以将控制权给运行时。这意味着我们需要某种可以等待的东西!
我们在清单 17-15 中已经可以看到这种控制权交接的发生:当我们移除未来值 `a` 末尾的 `trpl::sleep` 时,那么他将在未来值 `b` *完全* 未运行的情况下就完成。我们来尝试使用 `trpl::sleep` 函数作为让操作轮流取得进展的起点,如下列表 17-16 中所示。
<a name="listing_17-16"></a>
```rust
let one_ms = Duration::from_millis(1);
let a = async {
println!("'a' 已启动。");
slow("a", 30);
trpl::sleep(one_ms).await;
slow("a", 10);
trpl::sleep(one_ms).await;
slow("a", 20);
trpl::sleep(one_ms).await;
println!("'a' 已结束。");
};
let b = async {
println!("'b' 已启动。");
slow("b", 75);
trpl::sleep(one_ms).await;
slow("b", 10);
trpl::sleep(one_ms).await;
slow("b", 15);
trpl::sleep(one_ms).await;
slow("b", 350);
trpl::sleep(one_ms).await;
println!("'b' 已结束。");
};
```
**清单 17-16**:使用 `trpl::sleep` 让操作轮流取得进展
我们已在每个 `slow` 调用之间添加了带有等待点的 `trpl::sleep` 调用。现在两个值的工作是交错的:
```console
'a' 已启动。
'a' 运行了 30ms
'b' 已启动。
'b' 运行了 75ms
'a' 运行了 10ms
'b' 运行了 10ms
'a' 运行了 20ms
'b' 运行了 15ms
'a' 已结束。
```
在未来值 `a` 移交控制权给 `b` 之前,他仍然会运行一段时间,因为他在调用 `trpl::sleep` 之前先调用了 `slow`;但此后,每当其中一个未来值遇到等待点时,他们就会相互交替执行。在这一情形下,我们在每次调用 `slow` 之后都会这样做,但我们也可以对我们最合理的方式拆分工作。
不过,我们其实并不希望在这里 *休眠*:我们希望尽可能快地取得进展。我们只需要交还控制权给运行时即可。我们可以使用 `trpl::yield_now` 函数,直接做到这点。在下面清单 17-17 中,我们将所有 `trpl::sleep` 调用替换为 `trpl::yield_now`
<a name="listing_17-17"></a>
```rust
let a = async {
println!("'a' 已启动。");
slow("a", 30);
trpl::yield_now().await;
slow("a", 10);
trpl::yield_now().await;
slow("a", 20);
trpl::yield_now().await;
println!("'a' 已结束。");
};
let b = async {
println!("'b' 已启动。");
slow("b", 75);
trpl::yield_now().await;
slow("b", 10);
trpl::yield_now().await;
slow("b", 15);
trpl::yield_now().await;
slow("b", 350);
trpl::yield_now().await;
println!("'b' 已结束。");
};
```
**清单 17-17**:使用 `yield_now` 让操作轮流取得进展
这段代码不仅更清晰地表达了实际意图,而且比使用 `sleep` 快得多,因为诸如 `sleep` 使用的定时器,通常对其粒度有限制。例如,即使我们传递给他一个 1 纳秒的 `Duration`,我们使用的 `sleep` 版本也将最少休眠一毫秒。再说一次,现代计算机运行速度 *极快*:他们可以在一毫秒内执行大量操作!
这意味着,具体取决于咱们的程序正在执行的其他操作,异步甚至对于计算密集型任务也很有用,因为他为组织程序不同部分之间的关系,提供了一项有用的工具(但代价是异步状态机的开销)。这属于一种 *协作式多任务处理* 模式,每个未来值有权通过等待点,决定何时交出控制权。因此,每个未来值也有责任避免阻塞太久。在一些基于 Rust 的嵌入式操作系统中,这是 *唯一* 的多任务处理方式!
当然,在现实世界的代码中,咱们通常不会在每一行上都交替使用函数调用和等待点。虽然以这种方式让出控制权相对 “便宜”,但并不是毫无代价。在许多情况下,试图分解计算密集型任务,会使其速度显著变慢,因此有时简单地让操作阻塞,对 *整体* 性能会更好。请务必进行性能测试,以确定代码中实际的性能瓶颈所在。不过,当咱们大量咱们预期会并发进行的工作却在串行进行时,就要务必牢记这一底层机制!
## 构建我们自己的异步抽象
我们还可以组合未来值,创建新的模式。例如,我们可以我们已有的异步构建块,构建一个 `timeout` 函数。在我们完成后,结果将是另一个的构建块,我们可以用于创建更多的异步抽象。
下面清单 17-18 展示了我们将期望这个 `timeout` 怎样处理某个慢速未来值。
<a name="listing_17-18"></a>
```rust
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"最终完成"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("在 '{message}' 下成功"),
Err(duration) => {
println!("{} 秒后失败", duration.as_secs())
}
}
```
当我们从使用两个未来值,转换为使用三个未来值时,我们也不得不从使用 `join` 转换为使用 `join3`。如果每次改变我们要连接的未来值数量时,都要调用不同函数,那就太麻烦了。幸运的是,我们有个宏形式的 `join`,使用他我们就可以传递任意数量的参数。他还能自己处理等待未来值。因此,我们可以将清单 17-13 中的代码,重写为使用 `join!` 而非 `join3`,如下清单 17-14 中所示。
文件名:`src/main.rs`