Rust 并发收官:Futures、Tasks 与 Threads 如何协同工作(The Rust Programming Language 实战解析)
发布时间:2026/10/7 7:46:03 作者:尧图编辑部 阅读量:1,286
)
教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载导读本文对应 The Rust Programming LanguageRust 程序设计语言一书第 17 章异步编程的收官小节Putting It All Together: Futures, Tasks, and Threads。围绕何时用线程、何时用 async、能否同时用两者这一核心问题系统梳理 Rust 三种并发粒度的边界划分、运行时与工作窃取work stealing的底层机制并给出 CPU 密集与 I/O 密集两类场景的选型经验法则。读完本文你将掌握线程、任务task与 future 的组合用法并通过仓库中的trpl支持库源码与 Listing 17-25 完整示例理解多线程异步运行时在真实项目中的落地方式。开篇背景第 16 章的线程与第 17 章的 async本书在第 16 章介绍了线程这一并发途径而在第 17 章中我们看到了另一条路线基于async/await、future 与 stream 的异步编程。正如本章开篇ch17-00-async-await所指出的视频导出这类CPU-bound计算密集操作与视频下载这类I/O-bound输入输出密集操作分别对应两种截然不同的性能瓶颈也自然指向不同的并发手段。面对选线程还是选 async的疑问本文的答案非常明确看情况it depends而且在很多场景下答案并不是二选一而是线程和async 一起用。线程模型与 async 模型互补的两套取舍线程的代价与局限许多操作系统提供基于线程的并发模型已有数十年众多编程语言因此天然支持线程。但线程模型并非没有代价内存开销在许多操作系统上每个线程都会占用可观的内存硬件与系统依赖线程只有在操作系统与硬件支持时才可用嵌入式无 OS 场景与主流台式机、移动设备不同部分嵌入式系统根本没有操作系统因此也没有线程可用。这三点意味着线程是重量级的并发单元其生命周期完全由操作系统调度管理。async 模型由运行时管理的轻量任务async 模型提供了一套不同且最终互补的取舍并发操作不再各自独占一个线程而是运行在**任务task**之上——正如我们在 stream 小节用trpl::spawn_task从同步函数中启动异步工作那样。任务与线程很相似但关键区别在于管理方维度线程thread任务task管理方操作系统库级代码运行时runtime资源开销每个线程占用可观内存轻量可大量创建运行边界同步操作集合异步操作集合并发范围线程之间并发任务之间与任务内部均可并发正因如此spawn 线程与 spawn 任务的 API 看起来如此相似——它们都充当一组操作的边界只是前者界定同步操作、后者界定异步操作。而future 是 Rust 最细粒度的并发单元每个 future 可能代表一棵由其他 future 组成的树。整个层级关系是运行时确切地说是其执行器 executor管理任务任务管理 future。在这个意义上任务类似于由运行时管理的轻量线程并因由运行时而非操作系统管理而获得额外能力。三层并发模型线程 → 任务 → Future把三者放在一起看Rust 的并发粒度可以归纳为一个清晰的层级线程thread同步操作的边界并发发生在线程之间由操作系统调度任务task异步操作的边界由于任务体内可以在多个 future 之间切换所以并发既发生在任务之间也发生在单个任务内部future最细粒度的并发单元每个 future 可能是一棵 future 树例如通过trpl::join组合出的复合 future。这并不意味着 async 任务总是优于线程或反之。从某种角度看用线程做并发比用 async 做并发是更简单的编程模型——这可能是优点也可能是缺点。线程有点发射后不管fire and forget它没有与 future 天然对应的概念除了被操作系统打断外会一直运行到完成无法像 future 那样在 await 点主动挂起并把控制权交还给调度者。线程与任务为何常常相得益彰工作窃取Work Stealing线程和任务往往配合得非常好因为任务至少在部分运行时中可以在线程之间迁移。事实上本书一直在使用的运行时——包括spawn_blocking与spawn_task函数——默认就是多线程的许多运行时采用名为工作窃取work stealing的策略根据各线程当前的利用率透明地在线程之间迁移任务从而提升系统整体性能。这种策略本质上同时需要线程、任务和 future 三者线程提供真正的并行执行能力任务及构成任务的 future提供可挂起、可迁移的细粒度工作单元执行器把任务调度到当前最空闲的线程上执行。从源码层面可以印证这一点。本书配套的trpl支持库packages/trpl的核心职责就是转发其他 crate 的 API其spawn_task正是tokio::task::spawn的直接再导出见 lib.rs 第 45 行而trpl依赖的 Tokio 在 Cargo.toml 中通过rt-multi-threadfeature 启用了多线程运行时——这正是文档所述运行时默认多线程的实现事实。trpl::block_on的源码lib.rs 第 65-68 行也证实了这一点每次调用它都会新建一个tokio::runtime::Runtime并在其上运行传入的 future而该 Runtime 由rt-multi-threadfeature 配置为多线程工作窃取调度器。选型经验法则何时用线程何时用 async文档给出了两条非常实用的经验法则如果工作高度可并行即 CPU-bound——例如处理一批数据、且每个部分可以独立处理——线程是更好的选择。因为计算密集任务几乎不产生等待点让操作系统在线程间调度即可榨取多核并行能力async 的挂起机制在此毫无收益。如果工作高度并发即 I/O-bound——例如处理来自多个来源、可能以不同间隔或不同速率到达的消息——async 是更好的选择。等待 I/O 期间无需占用线程资源一个任务挂起后同一线程可以立即转去执行其他任务。而当并行与并发同时需要时你完全不必在二者之间做选择可以自由地把它们组合起来让各自发挥最擅长的部分。文档特别强调真实的 Rust 项目中这种混合极为常见。实战示例在线程中发消息、在 async 块中 awaitListing 17-25Listing 17-25 展示了一个相当典型的线程 async 混合模式用阻塞代码在线程中发送消息同时在一个 async 块中 await 这些消息。完整源码位于 listings/ch17-async-await/listing-17-25/src/main.rs如下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}); } }); }逐步拆解这段代码的执行流程创建异步通道trpl::channel()返回发送端tx与接收端rx。注意这里的接收端是mut的——与第 16 章基于std::sync::mpsc的同步通道不同异步版本的recv方法返回的是一个 future需要await而可变接收端是 await 期间轮询所要求的。在线程中持有发送端thread::spawn(move || ...)用move关键字把通道的发送端tx移入线程闭包。线程内部从 1 发送到 10每发一条就thread::sleep一秒——这是标准的阻塞式线程代码恰好演示了计算/阻塞密集部分交给线程的思路。在 async 块中 await 消息trpl::block_on启动一个 async 块while let Some(message) rx.recv().await循环等待消息。rx.recv()返回 futureawait 后消息到达即得Some(message)并打印发送端关闭后得到None循环结束程序随之退出。这与本书此前ch17-02-concurrency-with-async消息传递示例中的模式一致。回到本章开头的场景假设用专用线程跑一组视频编码任务因为视频编码是计算密集的但用异步通道通知 UI 这些操作已完成。线程负责闷头干活async 通道负责随时可挂起、不阻塞 UI的事件通知——二者各司其职。文档指出这类组合在真实世界的用例中数不胜数。关于trpl提供的通道从源码看它其实是 Tokio 无界通道的再导出lib.rs 第 41-44 行 将tokio::sync::mpsc::unbounded_channel重命名为trpl::channel因此发送端send无需await不会阻塞只有接收端recv是异步的。trpl之所以做这些简化按 lib.rs 顶部注释 的说法是为了让读者只需引入一个依赖、使用一套导入并保证示例始终可编译运行——这正是仓库中packages/trpl/tests/integration/main.rs集成测试存在的意义例如其中用trpl::spawn_task启动多个任务并断言其输出。小结与后续这并非本书最后一次涉及并发。第 21 章的项目ch21-00-final-project-a-web-server将在比本章更真实的场景中应用这些概念并更直接地对比用线程解决问题与用任务和 future 解决问题两种路径。无论你最终选择哪种方案Rust 都为你提供了写出安全、快速、并发代码所需的一切工具——无论目标是高吞吐的 Web 服务器还是嵌入式操作系统。再进一步本书接下来将转向 Rust 编程的惯用建模方式与结构组织方式并讨论 Rust 的惯用法与你可能熟悉的面向对象编程OOP之间的关系——这正是第 18 章ch18-00-oop的主题。参考资源仓库内本章正文src/ch17-00-async-await.md并行与并发的定义、CPU-bound 与 I/O-bound 区分异步并发实战src/ch17-02-concurrency-with-async.mdspawn_task、join的公平性、消息传递支持库源码packages/trpl/src/lib.rsblock_on、spawn_task、channel、sleep等 API 的实际实现支持库依赖配置packages/trpl/Cargo.tomlrt-multi-thread等 Tokio feature 的启用集成测试packages/trpl/tests/integration/main.rsspawn_task等 API 的可用性验证示例代码listings/ch17-async-await/listing-17-25/src/main.rs线程 async 通道混合示例赞分享教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载相关推荐Rust 官方《The Rust Programming Language》共享状态并发实战 — Mutex\T\ 与 Arc\T\ 深度解析Rust 官方《The Rust Programming Language》共享状态并发实战 — Mutex\T\ 与 Arc\T\ 深度解析 共享状态并教程文档Rust 异步编程基础《The Rust Programming Language》第 17 章 async/await、Futures 与 Streams 全解析Rust 异步编程基础《The Rust Programming Language》第 17 章 async/await、Futures 与 Streams教程文档Rust 开发工具实战指南rustfmt、rustfix、Clippy 与 rust-analyzer《The Rust Programming Language》附录 D 详解Rust 开发工具实战指南rustfmt、rustfix、Clippy 与 rust analyzer《The Rust Programming Langu教程文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考