Updated 'src/async/all_together.md'.

This commit is contained in:
Hector PENG
2026-04-15 12:44:52 +08:00
parent 68238dec7f
commit 37f854a0f5
5 changed files with 50 additions and 90 deletions

View File

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

View File

@@ -0,0 +1,18 @@
use std::{thread, time::Duration};
fn main() {
let (tx, mut rx) = trpl::channel();
thread::spawn(move || {
for i in 1..11 {
tx.send(i).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
trpl::block_on(async {
while let Some(message) = rx.recv().await {
println!("{message}");
}
});
}

View File

@@ -1,7 +1,4 @@
# 最后项目:构建一个多线程的 Web 服务器 # 最后项目:构建多线程的 Web 服务器
**Final Project: Building a Multithreaded Web Server**
这是一个漫长的旅程,但我们已经到达了本书的结尾。在本章中,咱们将一起构建又一个项目,来演示咱们在最后这些章中,曾涉及到的一些概念,同时回顾一些较早的内容。 这是一个漫长的旅程,但我们已经到达了本书的结尾。在本章中,咱们将一起构建又一个项目,来演示咱们在最后这些章中,曾涉及到的一些概念,同时回顾一些较早的内容。

View File

@@ -131,8 +131,8 @@
- [高级函数与闭包](advanced_features/adv_fns_and_closures.md) - [高级函数与闭包](advanced_features/adv_fns_and_closures.md)
- [关于宏](advanced_features/macros.md) - [关于宏](advanced_features/macros.md)
- [最后项目:构建一个多线程的 Web 服务器](Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md) - [最后项目:构建多线程的 Web 服务器](Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md)
- [构建一个单线程的 Web 服务器](final_project/single-threaded.md) - [构建单线程的 Web 服务器](final_project/single-threaded.md)
- [将这个单线程服务器修改为多线程服务器](final_project/multithreaded.md) - [将这个单线程服务器修改为多线程服务器](final_project/multithreaded.md)
- [优雅关机与内存清理](final_project/graceful_shutdown.md) - [优雅关机与内存清理](final_project/graceful_shutdown.md)

View File

@@ -1,77 +1,27 @@
# 在一起:未来值、任务与线程 # 组合在一起:未来值、任务与线程
正如我们在 [第 16 章](../Ch16_Fearless_Concurrency.md) 中所看到的,线程提供了一种并发方法。我们在章中看到了另一种方法:使用未来值与流异步。如果咱们想要知道,何时该选择一种方法,答案是:视情况而定!在很多情况下,我们需要选择不是线程 ** 异步,而是线程 ** 异步。 正如我们在 [第 16 章](../Ch16_Fearless_Concurrency.md) 中所看到的,线程提供了一种并发方法。我们在这一章中看到了另一种方法:通过未来值与流使用异步。当咱们在想何时该选择其中一种方法,答案是:视情况而定!而且在许多情况下,选择不是线程 ** 异步二者之一,而是线程 ** 异步兼用
数十年来,许多操作系统一直提供基于线程的并发模型,因此许多编程语言也支持这些模型。不过,这些模型并非没有权衡取舍。在许多操作系统中,每个线程都会占用相当多的内存。而且只有当咱们的操作系统和硬件都支持时,线程才是一种选择。不同于主流的台式机和便携电脑,一些嵌入式系统根本没有操作系统,因此他们也没有线程。
数十年来,许多操作系统都提供了基于线程的并发模型,而许多编程语言也因此支持这些模型。不过,这些模型也并非没有代价。在许多操作系统上,每个线程都会占用相当多的内存,而且启动和关闭线程都会产生一些开销。也只有在操作系统和硬件支持的情况下,线程才可用。与主流台式机和便携电脑不同,一些嵌入式系统根本没有操作系统,因此他们也没有线程 异步模型提供了一套不同的 -- 但最终是互补的 -- 权衡。在异步模型下,并发操作不需要自己的线程。相反,他们可以运行于任务上,就像我们在 [流小节](./streams.md) 中使用 `trpl::spawn_task` 启动同步函数中的工作一样。任务类似于线程,但他不是由操作系统管理,而是由库级别的代码(即运行时)管理
生成线程和生成任务的 API 如此相似,是有原因的。线程充当同步操作集的边界;线程 *之间* 的并发是可行的。任务充当异步操作集的边界;任务 *之间* 和任务 ** 的并发是可行的,因为任务可以在其主体中的不同未来值之间切换。最后,未来值属于 Rust 的最细粒度的并发单元,每个未来值都可以代表其他未来值的树。运行时 -- 具体来说,他的执行器 -- 管理任务,而任务则管理未来值。在这方面,任务类似于轻量级、运行时管理的线程,带有来自运行时而不是操作系统管理的附加功能。
异步模型提供了一套不同的权衡机制,而成为一种终极补充。在异步模型中,并发操作不需要其各自的线程。相反,他们可运行于任务之上,就像我们在流小节中,使用 `trpl::spawn_task` 启动某个同步函数的工作一样。任务类似于线程,但他不是由操作系统管理,而是由库级别的代码(即运行时)管理 这并不意味着异步任务总是比线程更好(反之亦然)。从某些方面来看,线程下的并发属于一种比 `async` 下的并发更简单的编程模型。这既可能是优势,也可能是劣势。线程在某种程度上是 “发射后不管” 的;他们没有与未来值相当的原生概念,因此除了被操作系统本身中断之外,他们会一直运行到完成
事实证明,线程和任务往往能很好地一起工作,因为任务可以(至少在某些运行时下)在线程之间迁移。事实上,在底层,我们一直在使用的运行时 -- 包括 `spawn_blocking``spawn_task` 函数 -- 默认就是多线程的!许多运行时采用一种称为 *工作窃取work stealling* 的方法,根据线程当前的利用情况,在线程之间透明地迁移任务,从而提升系统的整体性能。这种方法实际上同时需要 *线程**任务*,因此也需要未来值。
上一小节中,我们看到了可通过使用一个异步通道,及生成一个我们可从同步代码中调用的异步任务,而构建出一个流。我们也可使用线程,完成这完全一样的事情。在下面清单 17-40 中,我们使用标准库中的 `trpl::spawn_task``trpl::sleep` 两个 APIs替换了 `get_intervals` 中的异步通道与异步任务。 考虑何时使用哪种方法时,请参考以下经验法则:
- 当任务具有 *很强的并行性*(即 CPU 密集)时,例如处理大量数据,其中每个部分都可以单独处理,那么线程属于更好的选择;
- 当任务具有 *很强的并发性*(即 I/O 密集)时,例如处理来自多个不同来源的消息,这些消息可能以不同的间隔或速率传入,那么异步属于更好的选择。
而当咱们既需要并行性又需要并发性时,则不必在线程和异步之间选择。咱们可以自由地一起使用他们,让各自发挥所长。例如,下面 [清单 17-25]() 展示了现实 Rust 代码中此类混合使用的一个相当常见的示例。
<a name="listing_17-25"></a>
文件名:`src/main.rs` 文件名:`src/main.rs`
```rust
fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();
// This is *not* `trpl::spawn` but `std::thread::spawn`!
thread::spawn(move || {
let mut count = 0;
loop {
// Likewise, this is *not* `trpl::sleep` but `std::thread::sleep`!
thread::sleep(Duration::from_millis(1));
count += 1;
if let Err(send_error) = tx.send(count) {
eprintln!("Could not send interval {count}: {send_error}");
break;
};
}
});
ReceiverStream::new(rx)
}
```
*清单 17-41在 `get_intervals` 中使用 `std::thread` 而非异步的 `trpl` APIs*
若咱们运行这段代码,输出结果会与清单 17-40 相同。请注意,从调用代码的角度来看,这里的变化微乎其微。更重要的是,尽管我们的一个函数在运行时上生成了个异步任务,而另一函数生成了个操作系统的线程,但得到的两个流,并没有受到这些差异的影响。
尽管这两种方法有相似之处,但他们的行为却大相径庭,尽管我们可能很难在这个非常简单的例子中测量出来。我们可以在任何现代个人电脑上,生成数百万个异步任务。但若我们试图用线程来做这件事,内存真的会用完!
然而,这些 API 如此相似是有原因的。线程充当了一些同步操作集的边界;线程 *之间* 可以并发。任务则充当了一些异步操作集的边界;任务 *之间* 和任务 *内部* 都可以并发,因为任务可以在其主体中的未来值之间切换。最后,未来值是 Rust 最细粒度的并发单元,每个未来值都可以代表一棵由其他未来值组成的树。运行时 -- 具体来说是运行时的执行器 -- 管理着任务,而任务管理着未来值。在这方面,任务类似于由运行时管理着的轻量级线程,同时由于是由运行时而不是操作系统管理,因此任务还是具有更多功能的轻量级线程。
这并不意味着异步任务总是要比线程更好(反之亦然)。在某些方面,相比于使用 `async` 的并发,使用线程的并发是一种更简单的编程模型。这可以是优点,也可以是缺点。线程在某种程度上是 “触发并遗忘” 的;他们没有与未来值相对应的原生对等体,因此除非被操作系统本身打断,他们运行即可完成。也就是说,线程并不像未来值那样,支持 *任务内的并发*。Rust 中的线程也没有取消机制 -- 我们在本章中没有明确涉及这一主题,但每当我们结束某个未来值时,其状态就会被正确清理,这一事实暗示了任务的取消机制。
这些限制也使得线程比期货更难于组装。例如,使用线程构建 `timeout``throttle` 方法等辅助工具,就比我们在本章前面所构建的要困难得多。正如我们所看到的,未来值是一种更丰富的数据结构,这意味着他们可以更自然地组合在一起。
因此,任务给到我们对未来值的 *额外* 控制,允许我们选择在何处以及如何对他们分组。事实证明,线程和任务往往能配合得很好,因为任务可以(至少在某些运行时下)在线程间迁移。事实上,我们一直在使用的运行时,包括 `spawn_blocking``spawn_task` 两个函数,默认情况下都是多线程的!许多运行时都使用了一种名为 *工作偷取work stealing* 的方法,根据线程当前的使用情况,在线程间透明地迁移任务,以提高系统的整体性能。这种方法实际上需要线程 ** 任务,因此也需要未来值。
在考虑何时使用哪种方法时,请考虑以下经验法则:
- 如果工作的 *并行性很强*,比如处理每个部分都可以单独处理的大量数据时,线程是更好的选择;
- 如果工作的 *并发性很高*,例如处理来自不同来源,可能以不同时间间隔或不同速度发送的消息时,那么异步是更好的选择。
如果咱们同时需要并行性和并发性,咱们就不必在线程和异步之间做出选择。咱们可自由地将他们结合在一起使用,让他们各自发挥其最擅长的部分。例如,下面清单 17-42 展示了,实际 Rust 代码中这种混合使用的一个相当常见的示例。
文件名:`src/main.rs`
```rust ```rust
use std::{thread, time::Duration}; use std::{thread, time::Duration};
@@ -85,7 +35,7 @@ fn main() {
} }
}); });
trpl::run(async { trpl::block_on(async {
while let Some(message) = rx.recv().await { while let Some(message) = rx.recv().await {
println!("{message}"); println!("{message}");
} }
@@ -93,29 +43,17 @@ fn main() {
} }
``` ```
**清单 17-25**:在线程中以阻塞代码发送消息,并在异步代码块中等待消息
*清单 17-42在一个线程中以阻塞代码发送消息并在一个异步代码块中等待消息* 我们首先创建一个异步信道,然后生成一个线程,其使用 `move` 关键字取得该通道发送侧的所有权。在线程内,我们发送数字 1 到 10每次发送之间暂停一秒。最后我们通过以一个异步代码块创建的、并传递给 `trpl::block_on`,运行一个未来值,就像我们在这一整章中那样。在未来值中,我们等待这些消息,就像我们在其他消息传递示例看到的那样。
回到这一章开头的场景:设想使用一个专用线程来运行一组视频编码任务(因为视频编码是计算密集型的),而通过异步信道通知 UI 这些操作已经完成。在现实用例中,此类组合的示例不胜枚举。
我们以创建一个异步通道开始,然后生成一个取得通道发送侧所有权的线程。在该线程中,我们发送数字 1 到 10每个数字之间休眠一秒钟。最后就像本章所做的那样我们运行了一个以传递给 `trpl::run` 的异步代码块创建出的未来值。在这个未来值中,我们等待这些消息,就像在我们曾看到的其他消息传递示例中一样。 # 本章小结
这并不是咱们在这本书中最后一次看到并发。[第 21 章](../Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md) 中的项目,将在比这里讨论的简单示例更贴近实际的情境下应用这些概念,并更直接地比较分别通过线程、任务和未来值解决问题。
回到本章开头的场景,设想使用一个专门线程运行一组视频编码任务(因为视频编码是计算密集的),而以一个异步通道,通知用户界面这些操作已完成。在真实世界用例中,这类组合的例子数不胜数 无论咱们选择哪种方法Rust 都会给予咱们编写安全、高效、并发代码所需的工具 —— 无论是用于高吞吐量的 Web 服务器,还是嵌入式操作系统
## 本章小结
这并不是咱们在本书中最后一次看到并发。[第 21 章](../Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md) 中的项目,将在比这里讨论的简单示例更现实的情况下,应用这些概念,并更直接地比较使用线程和任务解决问题的方法。
无论咱们选择这些方法的哪种Rust 都能为咱们提供编写安全、快速、并发代码所需的工具,无论是用于高吞吐量 web 服务器,还是某种嵌入式操作系统。
接下来,我们将讨论在咱们的 Rust 程序变大时,问题建模和构建解决方案的一些惯用方法。此外,我们还将讨论 Rust 的惯用语,与咱们在面向对象编程中熟悉的惯用语之间的关系。
End
接下来,我们将探讨随着 Rust 程序规模的扩大,一些建模问题并构建解决方案的惯用方式。此外,我们还将讨论 Rust 的习惯用法,与咱们可能在面向对象编程中熟悉的那些习惯用法之间的关联。