From 3240c5d08cd75949f054071488ce1d6940f5bd45 Mon Sep 17 00:00:00 2001 From: Hector PENG Date: Sat, 11 Apr 2026 18:56:56 +0800 Subject: [PATCH] Updated 'src/async/multiple_futures.md'. --- projects/custom_async_abstraction/Cargo.toml | 7 + projects/custom_async_abstraction/src/main.rs | 19 ++ projects/starvation_demo/Cargo.toml | 7 + projects/starvation_demo/src/main.rs | 38 ++++ src/async/concurrency_n_async.md | 8 +- src/async/multiple_futures.md | 183 +++++++++++++++++- 6 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 projects/custom_async_abstraction/Cargo.toml create mode 100644 projects/custom_async_abstraction/src/main.rs create mode 100644 projects/starvation_demo/Cargo.toml create mode 100644 projects/starvation_demo/src/main.rs diff --git a/projects/custom_async_abstraction/Cargo.toml b/projects/custom_async_abstraction/Cargo.toml new file mode 100644 index 0000000..4b3efd2 --- /dev/null +++ b/projects/custom_async_abstraction/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "custom_async_abstraction" +version = "0.1.0" +edition = "2024" + +[dependencies] +trpl = "0.3.0" diff --git a/projects/custom_async_abstraction/src/main.rs b/projects/custom_async_abstraction/src/main.rs new file mode 100644 index 0000000..8fff4a7 --- /dev/null +++ b/projects/custom_async_abstraction/src/main.rs @@ -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); +} diff --git a/projects/starvation_demo/Cargo.toml b/projects/starvation_demo/Cargo.toml new file mode 100644 index 0000000..f5176db --- /dev/null +++ b/projects/starvation_demo/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "starvation_demo" +version = "0.1.0" +edition = "2024" + +[dependencies] +trpl = "0.3.0" diff --git a/projects/starvation_demo/src/main.rs b/projects/starvation_demo/src/main.rs new file mode 100644 index 0000000..c142f33 --- /dev/null +++ b/projects/starvation_demo/src/main.rs @@ -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); +} diff --git a/src/async/concurrency_n_async.md b/src/async/concurrency_n_async.md index 25c2c68..6c5e4e6 100644 --- a/src/async/concurrency_n_async.md +++ b/src/async/concurrency_n_async.md @@ -381,10 +381,10 @@ hi 来自第一个任务的数字 9 ! 我们已经探讨了 -- 如何使用消息传递,在未来值之间发送数据、 -- 异步代码块内的代码如何顺序执行、 -- 怎样迁移所有权到异步代码块中、 -- 以及怎样合并多个未来值。 +- 如何使用消息传递,在未来值之间发送数据 +- 异步代码块内的代码如何顺序执行 +- 怎样迁移所有权到异步代码块中 +- 以及怎样合并多个未来值 接下来,我们来讨论怎样以及为何要告知运行时,他可以切换到另一个任务。 diff --git a/src/async/multiple_futures.md b/src/async/multiple_futures.md index e679c4e..596ba60 100644 --- a/src/async/multiple_futures.md +++ b/src/async/multiple_futures.md @@ -1,7 +1,188 @@ # 使用任意数量的未来值 +## 将控制权交换给运行时 -在上一小节中,当我们从使用两个未来值,转换为使用三个未来值时,我们也不得不从使用 `join` 转换为使用 `join3`。如果每次改变我们要连接的未来值数量时,都要调用不同函数,那就太麻烦了。幸运的是,我们有个宏形式的 `join`,使用他我们就可以传递任意数量的参数。他还能自己处理等待未来值。因此,我们可以将清单 17-13 中的代码,重写为使用 `join!` 而非 `join3`,如下清单 17-14 中所示。 +回顾 [我们的第一个异步程序](./futures.md#我们的第一个异步程序),在每个等待点,Rust 都会给予运行时一个机会来暂定任务,并在等待的未来值尚未准备好时切换到另一任务。反之亦然:Rust 在等待点处 *仅* 暂停异步代码块,并交还控制权给运行时。等待点之间的一切都是同步的。 + +这意味着,当咱们在不带等待点的异步代码块中执行大量工作时,该未来值将阻塞任何其他未来值。咱们有时会听说这种情况被称为 “一个未来值 *饿死* 其他未来值”。在某些情况下,这可能无关紧要。然而,但咱们正在进行某种开销很高的设置或长时间运行的工作,或者有个未来值将无限期地执行某项特定任务时,咱们就需要考虑何时,以及在何处交还控制权给运行时。 + +我们来模拟一个长时间运行的操作,来演示 “饥饿” 问题,然后探讨怎样解决他。下面清单 17-14 引入了一个 `slow` 函数。 + + +```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 密集型的工作。 + + +```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 中所示。 + + +```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`。 + + +```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` 怎样处理某个慢速未来值。 + + + +```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`