本节摘要:thread::spawn 接收一个闭包作为新线程的案卷,闭包默认只借用环境、加 move 则整体带走产权;join 等待线程结案并取回返回值。读完你能安全地把数据送进线程,并理解"move 与不 move"的判决差异。
use std::thread; let exhibits = vec!["指纹", "鞋印"]; let handle = thread::spawn(move || { // move:整袋产权进线程 println!("清点 {} 件证物", exhibits.len()); exhibits.len() as u32 }); let count = handle.join().unwrap(); // 等待结案,取回返回值 println!("共 {} 件", count);
去掉 move 会怎样?闭包只借用 exhibits,而线程的生命周期可能超过当前作用域——借用悬垂,E0373 驳回。move 把产权移交线程,出借问题从根上消失。这就是第 3 章法条在跨线程场合的直接适用:要么给我,别借我。

let mut handles = Vec::new(); for i in 0..4 { handles.push(thread::spawn(move || { format!("分案 {} 完成", i) // 每个 i 是 Copy,各自带走 })); } for h in handles { println!("{}", h.join().unwrap()); }
⚠️ 常见坑:忘记 join,主线程先退庭导致整个进程结束,子线程没跑完就被终止。spawn 返回的 JoinHandle 就是"未结案证明",收下来逐个 join 是纪律。
spawn 的闭包必须活过当前函数,而普通闭包默认借用环境,两者冲突的第一现场永远是 E0373。
use std::thread; fn main() { let dossier = String::from("卷宗173"); // let h = thread::spawn(|| { // println!("{}", dossier); // E0373:闭包可能活得比借用久 // }); let h = thread::spawn(move || { // move:把产权带进新线程 println!("接办 {}", dossier); }); // println!("{}", dossier); // E0382:产权已移交闭包 h.join().expect("子线程不应 panic"); }
move 不是并发关键字,只是把捕获方式从借用改为夺取;真正逼你用它的是 spawn 的签名要求闭包满足 'static。需要两边共用时,答案不是 clone 就是 Arc,前者廉价隔离,后者共享计数。
join 返回 Result<T, Box<dyn Any + Send>>:成功拿返回值,失败拿 panic 载荷。丢掉 join 的问题不只是不等待——子线程 panic 会无声消失,故障排查时缺了第一证物。
use std::thread; fn main() { let handlers: Vec<thread::JoinHandle<u32>> = (1..=3) .map(|i| thread::spawn(move || { if i == 2 { panic!("第 {} 号档案缺失", i); } i * 100 })) .collect(); for (n, h) in handlers.into_iter().enumerate() { match h.join() { Ok(v) => println!("线程{}结案 {}", n, v), Err(e) => { let msg = e.downcast_ref::<&str>() .map(|s| *s) .unwrap_or("非字符串载荷"); println!("线程{}panic:{}", n, msg); } } } }
这段代码值得跑一次:线程 1 的 panic 被主线程接住并转写为日志,进程正常退出。线程池与任务运行时的"隔离爆炸半径"设计,原型就是这段。
标准库的 scope 接口放宽了 'static 限制:它保证借用范围内所有线程 join 完才返回,借用安全由作用域证明。
use std::thread; fn main() { let records = vec![String::from("甲"), String::from("乙")]; thread::scope(|s| { for r in &records { s.spawn(move || println!("处理 {}", r)); // 借用即可,无需 Arc } }); // 此处隐式 join 全部子线程 }
判据:数据生命周期天然覆盖线程(借用可行)用 scope;线程要带着数据逃逸(如常驻 worker)才用 Arc + move。
| 需求 | 写法 |
|---|---|
| 子任务独立数据 | spawn(move |
| 多线程共享只读 | Arc,T 无需锁 |
| 共享可变 | Arc<Mutex> |
| 借用主线程数据 | thread::scope |
| 常驻后台 | move + channel 汇报 |
最后一行的提醒:常驻线程必须设计退出信号(channel 断开或原子标志),否则进程退出时的悬挂线程会成为资源泄漏与测试超时的常客。join 是纪律,退出协议是契约,两者配齐才算完整并发设计。
CPU 密集任务的线程数上限取核数(std::thread::available_parallelism 可查),多开只增加切换开销;IO 密集任务线程数可放宽到等待时长的倒数倍,但真正海量的等待应交给下一章的消息队列与第三节的异步。一个实用锚点:线程栈默认约数 MB,数千线程的内存占用本身就成为瓶颈——遇到"再开两千个线程"的需求时,正确答案几乎总是线程池或异步改造。把"每任务一线程"限制在几十这个量级,是并发设计的第一条朴素纪律。