rust线程
Rust 的线程模型基于底层操作系统的 1:1 线程模型,这意味着一个系统线程直接对应一个 Rust 线程。Rust 通过其独创的所有权(Ownership)和借用(Borrowing)机制,在编译期就能杜绝并发编程中最头疼的“数据竞争”问题。
1. 基础:创建线程与等待
在 Rust 中,使用标准库的
std::thread::spawn 可以轻松创建一个新线程。use std::thread;
use std::time::Duration;
fn main() {
// 创建一个新线程
let handle = thread::spawn(|| {
for i in 1..5 {
println!("新线程: {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..3 {
println!("主线程: {}", i);
thread::sleep(Duration::from_millis(1));
}
// join() 会阻塞当前线程,直到 handle 对应的线程执行完毕
handle.join().unwrap();
}
- 主线程结束,程序即结束:如果主线程先执行完,新线程会被直接强行终止。
join()方法:通过调用handle.join(),主线程会等待子线程运行结束,确保任务完整执行。
1). 为什么
handle.join() 后面必须加 .unwrap()?在 Rust 中,
handle.join() 返回的不是普通的 Result<T, E>,而是一个特殊的类型:Result<T, Box<dyn Any + Send + 'static>> [1]当子线程运行结束时,主线程通过
join() 会收到两种完全不同的执行结果。加 .unwrap() 的本质,是为了在子线程发生崩溃(Panic)时,让主线程也跟着一起崩溃,防止错误被无意中掩盖。我们可以把这两类结果拆解来看:
- Ok(T):子线程正常运行结束。
.unwrap()会直接解包并返回子线程闭包中的返回值T[1]。 - Err(Box<dyn Any + Send>):子线程在运行过程中发生了崩溃(Panic) [1]。Rust 的线程设计具有“Panic 隔离”特性:一个子线程挂了,默认不会导致整个进程挂掉。如果你不用
.unwrap()去解包,主线程就会拿到这个Err并继续若无其事地往下走,这在大多数并发业务中是非常危险的。通过.unwrap(),一旦子线程崩溃,主线程在解包时也会立即触发 Panic(即崩溃传递),确保系统能够及时暴露问题。
线程状态
1). Joinable(可连接状态)
在 Rust 中,通过
std::thread::spawn 创建的线程默认都是 Joinable 的。特点
- 生命周期受控:主线程可以(也通常应该)显式等待该线程结束。
- 资源回收机制:当子线程执行完毕后,它的退出状态和部分底层资源会保留在系统中,直到有其他线程对其调用
.join(),这些资源才会被彻底清空。 - 返回值传递:Joinable 线程允许将子线程中最后一行表达式的结果(或错误)返回给调用
.join()的线程。
Rust 中的表现:
JoinHandlethread::spawn 会返回一个名为 JoinHandle<T> 的结构体。它就是操作该线程的“把手”。2). Detached(分离状态)
分离状态意味着线程与创建它的父线程“断开”了联系。一旦线程被分离,它将完全脱离宿主线程的控制,由操作系统独立管理。
特点
- 无法被等待:你不能对一个分离的线程调用
.join(),也无法得知它何时结束。 - 无法获取返回值:子线程的计算结果或崩溃信息(Panic)无法直接传递回主线程。
- 自动回收资源:当分离的线程执行完毕后,操作系统会自动立即回收它所占用的所有资源,不会留下任何痕迹。
- 随主线程消亡:虽然它被称为“独立”,但如果主线程(main 进程)退出了,所有分离的子线程依然会被操作系统强制杀死。
Rust 中如何创建分离线程?
在 Rust 中,你不需要显式调用类似
detach() 的函数。Rust 巧妙地利用了所有权系统(Drop 机制)来实现分离:规矩:如果你让JoinHandle离开了作用域(被销毁/Drop),且没有调用.join(),那么这个线程在底层就会被隐式地转为分离(Detached)状态。
2. 线程安全的关键:move 闭包
当你想在子线程中使用主线程的变量时,普通的闭包会因为“借用检查”而报错(因为 Rust 无法确定主线程的变量能活得比子线程久)。此时必须使用
move 关键字。use std::thread;
fn main() {
let v = vec![1, 2, 3];
// 使用 move 关键字将 v 的所有权强制转移(move)到新线程中
let handle = thread::spawn(move || {
println!("子线程使用 vector: {:?}", v);
});
// 此处不能再使用 v,因为所有权已经没了
// println!("{:?}", v); // 编译报错!
handle.join().unwrap();
}
3. 线程间通信:通道(Channel)
Rust 标准库提供了
mpsc(Multi-producer, Single-consumer,多生产者,单消费者)通道,专门用于线程间传递数据。use std::sync::mpsc;
use std::thread;
fn main() {
// 创建通道,tx 是发送端(Sender),rx 是接收端(Receiver)
let (tx, rx) = mpsc::channel();
// 克隆一个发送端,用于多个生产者
let tx1 = tx.clone();
// 生产者线程 1
thread::spawn(move || {
tx1.send(String::from("来自线程 1 的问候")).unwrap();
});
// 生产者线程 2
thread::spawn(move || {
tx.send(String::from("来自线程 2 的问候")).unwrap();
});
// 消费者(主线程)接收数据
for received in rx {
println!("收到: {}", received);
}
}
send方法会转移数据的所有权,防止发送后数据被修改引发混乱。- 当所有发送端(
tx)都被释放时,迭代器rx会自动结束循环。
4. 共享内存并发:Arc 与 Mutex
如果你需要多个线程同时读写同一块内存数据,通道就不适用了。Rust 使用原子引用计数(Arc)和互斥锁(Mutex)来实现安全的共享内存。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Mutex 提供互斥访问,Arc 提供跨线程的多重所有权
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// lock() 获取锁,如果锁被占用则等待。
// lock_num 是一个智能指针(MutexGuard),离开作用域时自动解锁!
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("最终计数结果: {}", *counter.lock().unwrap());
}
5. 线程安全底层:Send 与 Sync 特征
Rust 的并发安全并不是魔法,而是基于类型系统的两个内置特质(Trait):
Send:表明该类型的数据可以转移到其他线程(绝大多数类型都是Send,但Rc<T>不是)。Sync:表明该类型的数据可以被多个线程同时通过引用传递(如果T是Sync,那么&T就是Send)。
只要你在编写并发代码时违反了这两个特质,编译器就会直接拒绝编译,从而在源头上杜绝了运行时的并发 Bug。
💡 进阶建议
参考资料:
《Rust 权威指南》
浙公网安备 33010602011771号