文章目录
- [Rust 多线程从入门到实战](#Rust 多线程从入门到实战)
Rust 多线程从入门到实战
在多核处理器普及的今天,多线程编程早已成为提升程序性能的核心手段。但在多线程开发中,数据竞争、死锁、野指针等问题常常让人头疼,调试难度极大。而 Rust 凭借其独特的所有权系统和类型安全设计,将这些并发隐患扼杀在编译期,实现了无畏并发(Fearless Concurrency),让开发者既能享受多线程的性能优势,又无需担心内存安全问题。
并发与并行的区别
在开始编写多线程代码前,我们先厘清两个容易混淆的概念,这对理解Rust多线程的设计理念至关重要:
- 并发(Concurrency):多个任务在同一时间段内交替执行,即使是单核 CPU,通过任务调度也能实现,比如交替处理多个网络请求,核心是任务切换。
- 并行(Parallelism):多个任务在同一时刻真正同时执行,需要多核 CPU 的支持,比如多个核心同时处理不同的计算任务,核心是同时执行,是真正的多任务并行。
Rust 对两者都提供了完善的支持,而我们常说的多线程编程,既可以实现并发,也可以实现并行,具体取决于 CPU 核心数和程序调度。
创建与管理线程
最简单的多线程示例
使用 thread::spawn 可以快速创建一个新线程,该函数接收一个闭包作为参数,闭包中是新线程要执行的任务:
rust
use std::thread;
use std::time::Duration;
fn main() {
// 创建新线程,执行闭包中的任务
thread::spawn(|| {
for i in 0..=5 {
println!("子线程:执行第{}次", i);
// 让线程休眠100毫秒,模拟任务耗时
thread::sleep(Duration::from_millis(100));
}
});
// 主线程执行自己的任务
for i in 0..=2 {
println!("主线程:执行第{}次", i);
thread::sleep(Duration::from_millis(100));
}
thread::sleep(Duration::from_millis(1000));
}
运行上述代码,你会发现输出结果是乱序的,主线程和子线程在交替执行,这就是并发的体现。
等待线程完成:JoinHandle
thread::spawn 函数的返回值是一个 JoinHandle<T> 类型,其中 T 是子线程闭包的返回值类型。调用 join() 方法可以让主线程阻塞,等待子线程执行完毕,并获取子线程的返回值。
rust
use std::thread;
fn main() {
// 创建子线程,获取 JoinHandle
let handle = thread::spawn(|| {
let sum: i32 = (1..=100).sum();
sum // 子线程返回求和结果
});
// 主线程等待子线程完成,并获取返回值
// join() 返回 Result 类型,unwrap() 用于提取成功结果
// 实际开发中需处理错误情况,避免 panic
let result = handle.join().unwrap();
println!("子线程计算结果:1+2+...+100 = {}", result);
}
这里有一个关键细节:Rust 的所有权系统会严格检查线程间的数据访问。如果闭包中使用了主线程的变量,必须通过 move 关键字将变量的所有权转移到子线程中,否则会编译失败。
线程与所有权:move 关键字的作用
Rust 不允许线程间共享所有权,这是为了避免数据竞争,因此当子线程需要使用主线程的变量时,必须通过 move 将变量所有权转移。如下所示:
rust
use std::thread;
fn main() {
let msg = String::from("Hello, Rust!");
// 使用 move 将 msg 的所有权转移到子线程
let handle = thread::spawn(move || {
println!("子线程收到消息:{}", msg);
});
handle.join().unwrap();
// 注意:此时 msg 的所有权已转移,主线程无法再使用 msg
}
线程间通信
多线程协作的核心是通信,线程之间需要交换数据才能完成复杂任务。Rust 提供了两种主流的线程间通信方式:消息传递和共享状态,其中消息传递是 Rust 推荐的方式,不要通过共享内存来通信,而要通过通信来共享内存。
消息传递:mpsc 通道
Rust标准库中的 std::sync::mpsc 模块(multi-producer, single-consumer)提供了"多生产者、单消费者"的通道,允许多个线程向一个线程发送消息,实现安全的线程间通信。
通道的核心是两个角色:发送者(Sender)和接收者(Receiver),通过 mpsc::channel() 函数创建,返回一个元组(Sender, Receiver)。
rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
// 创建通道:发送者(tx)和接收者(rx)
let (tx, rx) = mpsc::channel();
// 创建第一个子线程(生产者1)
let tx1 = tx.clone(); // 克隆发送者,实现多生产者
thread::spawn(move || {
let messages = vec!["消息1", "消息2", "消息3"];
for msg in messages {
tx1.send(msg).unwrap(); // 发送消息
thread::sleep(Duration::from_millis(500));
}
});
// 创建第二个子线程(生产者2)
let tx2 = tx.clone();
thread::spawn(move || {
let messages = vec!["消息A", "消息B", "消息C"];
for msg in messages {
tx2.send(msg).unwrap();
thread::sleep(Duration::from_millis(500));
}
});
// 主线程作为消费者,接收所有消息
for received in rx {
println!("主线程收到:{}", received);
}
}
共享状态:Arc 与 Mutex 的组合
虽然消息传递是推荐方式,但在某些场景下,比如多个线程需要读写同一个变量,我们需要使用共享状态。Rust 通过原子引用计数(Arc)和互斥锁(Mutex)的组合,实现安全的共享状态访问。
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 创建共享计数器:Arc 包裹 Mutex,确保线程安全
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
// 创建10个线程,每个线程对计数器加1
for _ in 0..10 {
// 克隆 Arc,仅克隆引用,不复制数据
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 获取锁,返回 MutexGuard,实际开发中需处理可能的锁定失败,这里简化使用 unwrap()
let mut num = counter.lock().unwrap();
*num += 1;
// MutexGuard 离开作用域时,自动释放锁
});
handles.push(handle);
}
// 等待所有线程完成
for handle in handles {
handle.join().unwrap();
}
// 最终结果:10个线程各加1,结果为10
println!("最终计数器值:{}", *counter.lock().unwrap());
}
除了 Mutex,Rust 还提供了 RwLock(读写锁),适用于读多写少的场景,即多个线程可同时读取数据,只有一个线程能写入数据,能提升并发性能。
进阶:原子类型与 Send/Sync 特征
原子类型:无锁并发
对于简单的共享变量(如计数器),使用 Mutex 会有一定的性能开销(锁的获取和释放)。Rust 标准库中的 std::sync::atomic 模块提供了原子类型(如 AtomicUsize、AtomicBool),无需加锁即可实现线程安全的读写操作,性能更优。
rust
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
// 创建原子计数器,初始值为0
let counter = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 原子递增,使用 Relaxed 表示不保证内存顺序
counter.fetch_add(1, Ordering::Relaxed);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
// 读取原子类型的值
println!("原子计数器值:{}", counter.load(Ordering::Relaxed));
}
Send 与 Sync:Rust 的线程安全标记
Rust 通过两个特殊的特征(trait)来标记类型的线程安全性,编译器会自动检查,确保线程间的数据访问安全:
Send:表示类型的所有权可以安全地在线程间转移(如String、Arc)。Sync:表示类型的引用可以安全地在多个线程间共享(如Mutex<T>、原子类型)。
大多数 Rust 标准类型(如 i32、Vec<T>)都自动实现了 Send 和 Sync,但也有例外:
Rc<T>:未实现Send和Sync,只能用于单线程。- 裸指针(
*const T、*mut T):未实现Send和Sync,需手动保证安全。
无需手动为类型实现 Send 和 Sync,编译器会自动推导,只有当你实现自定义同步原语时,才需要手动实现,并确保其线程安全。
总结
Rust 的多线程设计看似复杂,实则是为了安全与性能的平衡。只要掌握了所有权和同步原语的核心逻辑,就能轻松编写高效、安全的多线程程序。后续可以深入学习异步编程,进一步提升并发性能。