Rust std::thread:spawn、join 与 move
更新时间:2026-09-20。Rust 标准库直接提供 1:1 的 OS 线程。它的特别之处是编译器在
spawn处就用生命周期 + Send 约束保证线程安全,不需要你运行时小心。并发总览见 无畏并发深稿,跨线程共享见 Arc/Mutex/RwLock。
一、最小线程与 join
use std::thread;
let h = thread::spawn(|| {
let mut sum = 0;
for i in 1..=100 { sum += i; }
sum // 线程可以有返回值
});
let result = h.join().unwrap(); // join 阻塞直到结束,取回 Result<i32>
assert_eq!(result, 5050);spawn 返回 JoinHandle<T>,join 拿回 thread::Result<T>:线程正常结束是 Ok(T),内部 panic 是 Err,panic 不会跨 join 直接炸掉父线程。
二、move 闭包:为什么必须 move
let data = vec![1, 2, 3];
let h = thread::spawn(move || { // 拿走 data 所有权
println!("{:?}", data);
});
h.join().unwrap();
// println!("{:?}", data); // 编译错误:data 已被 move新线程可能比当前作用域活得久,普通引用 &data 无法保证数据在线程结束前仍有效,所以编译器要求 move 把捕获变量的所有权搬进线程。只想让多线程共享同一份数据时,用 Arc(见 Arc/Mutex/RwLock)。
三、Builder:命名、栈大小
let h = thread::Builder::new()
.name("worker-1".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
println!("我是 {:?}", thread::current().name());
})?;
h.join().unwrap();给线程命名后,panic 信息和调试器里能直接看到 worker-1,排查多线程问题必备。sleep 用 thread::sleep(std::time::Duration::from_millis(50))。
四、与 Send 的关系
spawn 的签名要求闭包是 Send + 'static:'static 禁止借用短生命周期数据,Send 保证捕获类型能安全转移到另一线程(如 Rc 不是 Send,编译就拦下)。详见 Send/Sync 标记 trait。
五、常见坑
- 忘记
join:主线程(main)结束会直接终止整个进程,子线程来不及跑完;需要结果或保序就join。 - 不写
move想借用局部变量:生命周期编译错误,move或改用Arc。 - 把
Rc/RefCell搬进线程:非 Send/Sync 编译失败,换Arc/Mutex。 join().unwrap()忽略 panic:注意join返回 Result,按需记录Err。- 海量任务各开一个 OS 线程:创建/调度成本高,CPU 密集用 rayon 数据并行或线程池,I/O 密集考虑 async。
总结
std:🧵:spawn 接一个闭包、返回 JoinHandle,join 阻塞等待并取回 thread::Result(panic 被包成 Err 而非炸掉父线程);因为新线程生命周期可能超出当前作用域,捕获外部数据必须用 move 拿走所有权或用 Arc 共享,闭包还要满足 Send + 'static,所以 Rc/RefCell 这类非线程安全类型在编译期就被拒;忘记 join 会随 main 退出被强行终止,命名线程、按任务类型选 rayon/async 而非无脑开线程。