comprehensive-rust 并发入门:深入解析 Rust 线程(Plain Threads 与 Scoped Threads)
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
本文以 Google Android 团队维护的 Rust 培训课程 comprehensive-rust 中"Threads"章节为核心,系统讲解 Rust 中基于操作系统线程的并发编程:如何通过std::thread::spawn创建线程、如何用JoinHandle等待线程结束并取回返回值、如何处理线程 panic,以及如何用thread::scope让线程安全地借用外部数据。读完本文,你将掌握 Rust 线程从创建、同步到安全借用数据的完整实战路径,并理解"无惧并发(fearless concurrency)"在类型系统层面的保障原理。
一、课程脉络:Threads 章节在 comprehensive-rust 中的位置
在 comprehensive-rust 的 并发(Concurrency)课程 中,线程是第一天上午的第一个主题,由三部分构成(见 src/SUMMARY.md):
- Threads:章节总览页,通过
{{%segment outline}}占位符自动渲染本节大纲; - Plain Threads:基础线程(建议授课 15 分钟);
- Scoped Threads:作用域线程(建议授课 13 分钟)。
课程定位明确指出:Rust 使用操作系统线程搭配互斥锁(mutex)与通道(channel)实现完整的并发支持,而类型系统把大量并发 bug 变成编译期错误——这正是"无惧并发"的核心思想。线程章节正是这一理念的第一站:即使不引入任何特殊并发语法,借用检查器也能保证线程间数据访问的内存安全。
补充说明:总览页 src/concurrency/threads.md 本身只有标题与
{{%segment outline}}宏。这个宏并非普通 Markdown,而是由仓库中的构建工具 mdbook-course 在渲染时替换。在 mdbook-course/src/replacements.rs 中,["segment", "outline"]会被解析为调用当前 Segment 的outline()方法;其底层实现在 mdbook-course/src/course.rs,按章节顺序生成"Plain Threads / Scoped Threads"的课程大纲列表。这也解释了为什么本小节的知识主体分布在两个子页面中。
二、Plain Threads:用thread::spawn创建第一个线程
Rust 线程的用法与其他语言非常相似。以下来自 src/concurrency/threads/plain.md 的完整示例展示了主线程与子线程并行计数:
use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 0..10 { println!("Count in thread: {i}!"); thread::sleep(Duration::from_millis(5)); } }); for i in 0..5 { println!("Main thread: {i}"); thread::sleep(Duration::from_millis(5)); } }代码要点如下:
thread::spawn(closure)创建线程:std::thread::spawn接收一个闭包,闭包即新线程要执行的代码。这里通过thread::sleep各睡 5ms,让主线程与子线程基本保持同步推进(5ms 的粒度足够宽松,二者输出会交替出现)。- 子线程不阻塞
main结束:运行上面的例子会发现,程序往往在子线程数到 10 之前就退出了。原因在于main函数返回即进程结束,而thread::spawn创建的线程不会被自动等待。这一点与 C++ 的std::thread/boost::thread/ POSIXpthreads的 join 语义形成对比——那些语言的线程对象析构时通常需要显式 join 或 detach,而 Rust 默认"不等待"。 - 线程 panic 相互独立:某个线程发生 panic 不会导致其他线程或主线程崩溃。panic 还可以携带 payload(载荷),事后可通过
Any::downcast_ref拆解出具体类型(详见下文)。
2.1 用JoinHandle::join()等待线程完成
既然子线程不阻塞进程退出,如何等待它跑完?thread::spawn的返回值正是答案:它返回一个JoinHandle,其上提供阻塞式的.join()方法。
use std::thread; fn main() { let handle = thread::spawn(|| { for i in 0..10 { println!("Count in thread: {i}!"); } }); handle.join().unwrap(); // 至此子线程必然已经结束,主线程才会继续 }把thread::spawn(...)的返回值保存为let handle = ...,再在需要同步的位置调用handle.join(),主线程就会阻塞等待子线程执行完毕,程序从而可以完整计数到 10。
2.2 从线程取回返回值:thread::Result<T>
thread::spawn的闭包返回类型是T,而JoinHandle::join()的返回类型是thread::Result<T>——即Result<T, Box<dyn Any + Send + 'static>>。换句话说:
- 闭包里的
return值,可以通过join()的Ok分支取回; - 若线程 panic,
join()返回Err,其中携带 panic 的 payload。
use std::thread; fn main() { let handle = thread::spawn(|| { let mut sum = 0; for i in 0..10 { sum += i; } sum // 闭包的返回值 T = i32 }); match handle.join() { Ok(value) => println!("线程返回值:{value}"), Err(payload) => println!("线程 panic,载荷:{payload:?}"), } }Err(payload)中的payload是Box<dyn Any + Send>,这正是课程中顺带讲解Any的时机:可以用payload.downcast_ref::<String>()等操作把 panic 载荷还原为具体类型。
2.3 线程如何"取输入":闭包捕获与move
返回值解决了线程"向外传数据"的问题,那"向内传数据"呢?闭包天然支持捕获外部变量:
use std::thread; fn main() { let data = String::from("Hello"); let handle = thread::spawn(move || { // 使用 data 计算派生值 data.len() }); let _ = handle.join(); }这里有一个关键教训:直接按引用捕获通常会触发编译错误。因为thread::spawn要求闭包满足'static生命周期,普通借用无法保证跨线程存活,编译器会提示"must move it"。解决办法是显式move,把变量所有权移入线程闭包,在闭包内完成计算并返回派生值。这也自然引出了下一个问题——如果想借用而非转移所有权,该怎么办?
三、Scoped Threads:用thread::scope安全借用外部数据
普通线程无法借用环境中的数据。下面这段代码来自 src/concurrency/threads/scoped.md,编译必然失败:
use std::thread; fn foo() { let s = String::from("Hello"); thread::spawn(|| { dbg!(s.len()); }); } fn main() { foo(); }失败的根本原因非常深刻:如果foo()返回而子线程还在运行,子线程访问的s将指向已失效的栈帧,造成"栈上 use-after-return",直接违反内存安全。普通thread::spawn创建的线程没有生命周期约束,Rust 编译器宁可拒绝编译也不允许悬垂引用。
3.1thread::scope:带生命周期的有界线程
解决方案是标准库提供的作用域线程(scoped thread)thread::scope:
use std::thread; fn foo() { let s = String::from("Hello"); thread::scope(|scope| { scope.spawn(|| { dbg!(s.len()); }); }); } fn main() { foo(); }同样的代码,把thread::spawn换成thread::scope(|scope| scope.spawn(...))后即可编译通过。其安全保证来自一个简单的机制:
当
thread::scope的函数体执行完毕返回时,所有在该作用域内创建的线程都已被保证 join 完成。因此,这些线程可以安全地返回借用自外部栈的数据——借用期严格被限制在 scope 调用之内。
由于所有子线程的生命周期都封在thread::scope内部,借用检查器可以确认"子线程结束先于外部栈帧销毁",从而放行非'static的引用捕获。而作用域内部依然适用普通的 Rust 借用规则:
- 要么被一个线程可变借用(mutable borrow);
- 要么被任意多个线程不可变借用(shared borrow)。
这保证了即使在多线程场景下,也绝不可能出现数据竞争。
3.2 实战对比:什么时候用哪种 API
| 场景 | 推荐 API | 原因 |
|---|---|---|
| 线程需要借用调用方栈上的数据 | thread::scope | 生命周期被约束在作用域内,编译期保证安全 |
线程完全自包含、数据全部move进去 | thread::spawn | 无需等待所有线程结束,灵活度更高 |
| 需要等待线程完成并取回结果 | spawn+JoinHandle::join() | join 返回thread::Result<T>,可同时处理 panic |
| 需要让多个子线程并发读写不同数据 | thread::scope | 借用规则让编译器替你检查数据竞争 |
四、延伸阅读:从线程到完整并发工具箱
线程只是并发拼图的第一块。在 comprehensive-rust 课程中,紧接着 Channels(通道) 与 Shared State(共享状态) 主题,分别覆盖mpsc通道以及Arc+Mutex的组合——三者共同构成 Rust 标准库层的并发三件套:
- 消息传递:
std::sync::mpsc通道(有界/无界),线程间通过发送消息协作; - 共享内存:
Arc<Mutex<T>>在多个线程间共享可变状态; - 同步原语:配合
Send/Sync标记 trait 在编译期约束类型可否跨线程传递与共享。
所有练习可在 并发同步练习(如哲学家就餐问题、链接检查器)中巩固验证;对异步编程感兴趣的读者,可进一步学习课程中的 async/await 与 tokio 运行时。
五、小结
通过 comprehensive-rust 的 Threads 章节可以提炼出三条核心结论:
- 创建与同步:
thread::spawn(closure)创建线程,返回JoinHandle;handle.join()阻塞等待并返回thread::Result<T>,从而既能取回结果也能捕获 panic 载荷。 - 数据输入:闭包捕获外部变量需要
move转移所有权;若想借用,则必须改用thread::scope,其"返回前保证 join 所有线程"的语义让借用检查器能够安全放行非'static引用。 - 安全哲学:Rust 不依赖运行时检查来防数据竞争,而是把线程生命周期、借用与
Send/Sync约束全部交给编译期——这就是"无惧并发"的具体体现,也是本课程希望传授给学习者的核心思维方式。
相关源码与文档:线程章节正文见 src/concurrency/threads/plain.md 与 src/concurrency/threads/scoped.md;章节大纲的自动生成逻辑见 mdbook-course/src/replacements.rs 与 mdbook-course/src/course.rs;课程定位与日程见 src/concurrency/welcome.md。
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考