讲解
std::thread::spawn 创建操作系统级线程(1:1 模型),返回 JoinHandle——调用 .join() 等待线程结束并取回结果。最容易踩的坑是主线程先结束:不 join 的话,主线程退出时派生线程会被直接终止。闭包传给 spawn 时几乎总要带 move:线程可能活得比当前函数久,闭包必须拿走数据的所有权,借用局部变量编译器直接拒绝——「数据竞争在 Rust 里编译不过」就是从这些约束里长出来的。
线程间通信的 Rust 哲学是「不要通过共享内存来通信,要通过通信来共享内存」。std::sync::mpsc 通道(multiple producer, single consumer)是首选工具:channel() 返回 (tx, rx) 两端,tx.clone() 造出多个发送者,rx 逐个接收;send 会移动值的所有权,天然避免了「两边同时持有」的隐患。当所有发送端 drop 后,接收端的迭代器自然结束——忘了 drop 原始 tx 是接收端死等的经典原因。
确实需要共享可变状态时,工具是 Arc<Mutex>:Arc 是 Rc 的线程安全版(原子引用计数),Mutex 保证同一时刻只有一个线程能访问内部数据(lock() 拿到守卫,守卫离开作用域自动解锁)。类型系统在这里依然站岗:不把数据包进 Mutex 就想跨线程共享可变状态,编译器不给过——数据竞争从「线上偶发事故」降级为「写不出来」。
示例
spawn + join + move 闭包:
use std::thread;
fn main() {
let data = vec![1, 2, 3, 4, 5];
// move:线程拿走 data 的所有权(线程可能比 main 活得久)
let handle = thread::spawn(move || {
let sum: i32 = data.iter().sum();
println!("子线程求和:{sum}");
sum // join 可以取回这个返回值
});
// data 已移动,main 里不能再用
let result = handle.join().expect("子线程 panicked");
assert_eq!(result, 15);
println!("主线程收到结果:{result}");
}
mpsc 通道:多生产者发消息,收集后排序保证确定性:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let mut handles = vec![];
for i in 0..3 {
let sender = tx.clone(); // 每个线程一个发送端
handles.push(thread::spawn(move || {
sender.send(format!("消息 {i}")).unwrap();
}));
}
drop(tx); // 关键:原始发送端 drop,接收迭代才会结束
let mut received: Vec<String> = rx.iter().collect();
for h in handles {
h.join().unwrap();
}
received.sort(); // 到达顺序不定,排序后断言
assert_eq!(received, vec!["消息 0", "消息 1", "消息 2"]);
println!("共收到 {} 条消息", received.len());
}
共享状态:Arc<Mutex> 计数器:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let counter = Arc::clone(&counter); // 克隆计数,共享同一份
handles.push(thread::spawn(move || {
*counter.lock().unwrap() += 1; // 锁守卫离开作用域自动解锁
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(*counter.lock().unwrap(), 5);
println!("五个线程各加一,结果正确");
}
常见坑
- 忘记 join:主线程退出时派生线程被强制终止,任务可能跑一半;拿到 JoinHandle 就要想好谁来 join。
- 接收端死等:发送端(包括原始 tx)没全部 drop,rx.iter() 会一直等下去;多生产者场景记得 drop 手里的 tx。
- 以为共享可变状态能绕编译器:不包 Mutex 就跨线程改数据是编译错误——接受这个约束,它替你挡掉了最难排查的 bug 类别。
- 锁粒度太大:一个全局 Mutex 包住所有状态,线程退化成串行;按数据拆分锁,或干脆回到消息传递。
小结
spawn/move/join 是线程三件套;mpsc 通道用通信代替共享;必须共享时 Arc<Mutex> 由类型系统护航。教程最后一章:让代码自带验证——自动化测试。