# 组合在一起：未来值、任务与线程

正如我们在 [第 16 章](../Ch16_Fearless_Concurrency.md) 中所看到的，线程提供了一种并发方法。我们在这一章中看到了另一种方法：通过未来值与流使用异步。当咱们在想何时该选择其中一种方法时，答案是：视情况而定！而且在许多情况下，选择不是线程 *或* 异步二者之一，而是线程 *和* 异步兼用。

数十年来，许多操作系统一直提供基于线程的并发模型，因此许多编程语言也支持这些模型。不过，这些模型并非没有权衡取舍。在许多操作系统中，每个线程都会占用相当多的内存。而且只有当咱们的操作系统和硬件都支持时，线程才是一种选择。不同于主流的台式机和便携电脑，一些嵌入式系统根本没有操作系统，因此他们也没有线程。

异步模型提供了一套不同的 -- 但最终是互补的 -- 权衡。在异步模型下，并发操作不需要自己的线程。相反，他们可以运行于任务上，就像我们在 [流小节](./streams.md) 中使用 `trpl::spawn_task` 启动同步函数中的工作一样。任务类似于线程，但他不是由操作系统管理，而是由库级别的代码（即运行时）管理。

生成线程和生成任务的 API 如此相似，是有原因的。线程充当同步操作集的边界；线程 *之间* 的并发是可行的。任务充当异步操作集的边界；任务 *之间* 和任务 *内* 的并发是可行的，因为任务可以在其主体中的不同未来值之间切换。最后，未来值属于 Rust 的最细粒度的并发单元，每个未来值都可以代表其他未来值的树。运行时 -- 具体来说，他的执行器 -- 管理任务，而任务则管理未来值。在这方面，任务类似于轻量级、运行时管理的线程，带有来自运行时而不是操作系统管理的附加功能。

这并不意味着异步任务总是比线程更好（反之亦然）。从某些方面来看，线程下的并发属于一种比 `async` 下的并发更简单的编程模型。这既可能是优势，也可能是劣势。线程在某种程度上是 “发射后不管” 的；他们没有与未来值相当的原生概念，因此除了被操作系统本身中断之外，他们会一直运行到完成。

事实证明，线程和任务往往能很好地一起工作，因为任务可以（至少在某些运行时下）在线程之间迁移。事实上，在底层，我们一直在使用的运行时 -- 包括 `spawn_blocking` 和 `spawn_task` 函数 -- 默认就是多线程的！许多运行时采用一种称为 *工作窃取，work stealling* 的方法，根据线程当前的利用情况，在线程之间透明地迁移任务，从而提升系统的整体性能。这种方法实际上同时需要 *线程* 和 *任务*，因此也需要未来值。

在考虑何时使用哪种方法时，请参考以下经验法则：

- 当任务具有 *很强的并行性*（即 CPU 密集）时，例如处理大量数据，其中每个部分都可以单独处理，那么线程属于更好的选择；
- 当任务具有 *很强的并发性*（即 I/O 密集）时，例如处理来自多个不同来源的消息，这些消息可能以不同的间隔或速率传入，那么异步属于更好的选择。

而当咱们既需要并行性又需要并发性时，则不必在线程和异步之间选择。咱们可以自由地一起使用他们，让各自发挥所长。例如，下面清单 17-25 展示了现实 Rust 代码中此类混合使用的一个相当常见的示例。

<a name="listing_17-25"></a>
文件名：`src/main.rs`

```rust
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}");
        }
    });
}
```

**清单 17-25**：在线程中以阻塞代码发送消息，并在异步代码块中等待消息

我们首先创建一个异步信道，然后生成一个线程，其使用 `move` 关键字取得该通道发送侧的所有权。在线程内，我们发送数字 1 到 10，每次发送之间暂停一秒。最后，我们通过以一个异步代码块创建的、并传递给 `trpl::block_on`，运行一个未来值，就像我们在这一整章中那样。在未来值中，我们等待这些消息，就像我们在其他消息传递示例看到的那样。

回到这一章开头的场景：设想使用一个专用线程来运行一组视频编码任务（因为视频编码是计算密集型的），而通过异步信道通知 UI 这些操作已经完成。在现实用例中，此类组合的示例不胜枚举。


# 本章小结

这并不是咱们在这本书中最后一次看到并发。[第 21 章](../Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md) 中的项目，将在比这里讨论的简单示例更贴近实际的情境下应用这些概念，并更直接地比较分别通过线程、任务和未来值解决问题。

无论咱们选择哪种方法，Rust 都会给予咱们编写安全、高效、并发代码所需的工具 —— 无论是用于高吞吐量的 Web 服务器，还是嵌入式操作系统。

接下来，我们将探讨随着 Rust 程序规模的扩大，一些建模问题并构建解决方案的惯用方式。此外，我们还将讨论 Rust 的习惯用法，与咱们可能在面向对象编程中熟悉的那些习惯用法之间的关联。
