Rust 无畏并发编程
深入掌握 Rust 无畏并发(Fearless Concurrency):线程创建、通道消息传递、Arc 与 Mutex 状态共享及 Send/Sync 特征。
Rust 中级系列第 15 篇。并发编程(Concurrency)历来以高难度和易出错著称 —— 数据竞争(Data Race)、死锁以及内存破坏是多线程系统中最头疼的隐患。
Rust 提出了 Fearless Concurrency(无畏并发) 的理念:依靠所有权与类型系统,将并发安全隐患直接转化为编译期错误,从而在编译阶段杜绝绝大多数并发 Bug。
graph TD
A["Rust 无畏并发"] --> B["原生线程创建<br/>std::thread::spawn"]
A --> C["消息传递通信<br/>Channels (mpsc)"]
A --> D["共享状态并发<br/>Arc<T> + Mutex<T>"]
A --> E["可扩展并发安全<br/>Send & Sync 特征"]
浅显易懂(ELI5):繁忙的餐厅后厨#
想象一家餐厅的后厨工作:
- 单线程模式:只有 1 位大厨负责洗菜、切配、炒菜到洗碗的全部流程。绝对安全,但吞吐量有限。
- 缺乏管制的传统多线程(C/C++):5 位厨师在没有规则的情况下同时争抢同一把刀和案板,极易误伤彼此或切错食材(数据竞争与内存破坏)。
- Rust 无畏并发:每位厨师拥有专属工位(所有权),通过点单传送带传递菜品需求(通道 Channel)。当必须共用特制铜锅时,必须刷卡获取锁(Arc + Mutex)。主厨(编译器)在开门营业前会对所有协作规范进行严格审查!
1. 使用原生线程并发执行代码#
在 Rust 中,你可以使用 thread::spawn 创建操作系统原生线程:
src/main.rs
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));
}
// 等待子线程执行完毕
handle.join().unwrap();
}rust使用 move 闭包转移所有权#
当子线程需要使用外部作用域的数据时,可以通过 move 关键字显式将变量的所有权转移给线程闭包:
src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("线程内的动态数组: {v:?}");
});
handle.join().unwrap();
}rust2. 消息传递:通道(Channels mpsc)#
Go 社区的名言在 Rust 中同样适用:“不要通过共享内存来通信,而要通过通信来共享内存。”
Rust 标准库提供了 mpsc(multiple producer, single consumer:多生产者,单消费者)通道:
src/main.rs
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone(); // 克隆发送端以支持多个生产者
// 发送线程 1
thread::spawn(move || {
let msgs = vec!["你好", "来自", "线程 1"];
for msg in msgs {
tx1.send(String::from(msg)).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
// 发送线程 2
thread::spawn(move || {
let msgs = vec!["消息", "来自", "线程 2"];
for msg in msgs {
tx.send(String::from(msg)).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
// 主线程作为唯一消费者接收数据
for received in rx {
println!("收到消息: {received}");
}
}rust3. 共享状态并发:Arc<T> 与 Mutex<T>#
当多个线程需要共同读写同一块内存时:
Mutex<T>(互斥锁):保证同一时刻只允许一个线程持有锁并访问其包装的数据。Arc<T>(原子引用计数):一种线程安全的智能指针,允许多个线程共享同一个Mutex的所有权。
src/main.rs
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("最终计数结果: {}", *counter.lock().unwrap()); // 10
}rust[!NOTE]
Rc<T>并不是线程安全的,因为它的计数器变更未采用原子操作。Rust 编译器会直接拒绝跨线程传递Rc<T>,强制使用Arc<T>。
4. 并发基石:Send 与 Sync 特征#
Rust 的并发安全模型建立在两个核心 auto traits 之上:
Send:表明该类型的所有权可以在线程间安全传递。Sync:表明该类型的不可变引用(&T)可以在多个线程间安全共享(T: Sync等价于&T: Send)。
Rust 绝大多数基础类型默认都实现了 Send 和 Sync。若试图跨线程共享不安全的类型(如 Rc<T>),编译器会在构建阶段直接阻止编译。
总结#
thread::spawn配合move闭包实现安全的所有权转移与多线程启动。mpsc::channel基于所有权模型构建了可靠的多生产者单消费者通信。Arc<Mutex<T>>是跨线程安全共享与修改可变状态的标准组合拳。Send和Sync特征将数据竞争的预防深度融入了编译期类型检查。