在 Rust 中,“线程安全”不仅仅是一个概念,而是通过类型系统和编译器检查在编译阶段强制保证的特性。Rust 的核心哲学是:如果代码能编译通过,那么它在运行时就是线程安全的(没有数据竞争) 。
Rust 通过两个核心 Trait 来定义和管理线程安全性:Send 和 Sync。
1. 核心基石:Send 和 Sync
这两个 Trait 是 Rust 并发模型的自动推导基础,通常不需要手动实现(除非你在写 unsafe 代码)。
Send:所有权可以跨线程转移
-
定义:如果一个类型
T实现了Send,那么它的所有权可以安全地从当前线程移动(Move) 到另一个线程。 -
含义:该类型不包含任何“线程局部”资源或不可跨线程访问的指针。
-
例子:
i32,String,Vec<T>(当 T 是 Send 时) 都是Send的。Rc<T>(引用计数指针) 不是Send的,因为它的引用计数操作不是原子的,跨线程修改会导致数据竞争。*const T(原始指针) 默认不是Send的。
rust
编辑
use std::thread;
fn main() {
let s = String::from("Hello");
// String 实现了 Send,所以可以 move 进线程
thread::spawn(move || {
println!("{}", s);
}).join().unwrap();
}
Sync:引用可以跨线程共享
-
定义:如果一个类型
T实现了Sync,那么&T(对 T 的不可变引用) 可以安全地在多个线程间共享。 -
数学关系:
T是Sync的 当且仅当&T是Send的。 -
含义:允许多个线程同时读取该数据。注意:
Sync不意味着可以同时可变访问(那是Mutex的工作),它只保证并发读取是安全的。 -
例子:
i32,String,Vec<T>都是Sync的。Cell<T>,RefCell<T>不是Sync的,因为它们允许在只有不可变引用的情况下进行内部可变性修改,这在多线程下会导致数据竞争。Rc<T>不是Sync的。
2. 如何实现线程安全的可变共享?
既然普通的变量不能直接在多线程中可变共享,Rust 提供了以下模式:
A. 消息传递 (Message Passing) - 首选
使用 std::sync::mpsc 通道。数据的所有权在线程间转移,同一时刻只有一个线程拥有数据,从根本上避免竞争。
- 适用场景:管道处理、任务分发。
B. 共享内存 + 锁 (Shared Memory + Locks)
当必须共享状态时,使用智能指针包装互斥锁。
-
Arc<Mutex<T>>:Arc(Atomic Reference Counting): 提供线程安全的引用计数,允许多个线程拥有同一个数据的“所有权句柄”。Mutex: 保证同一时间只有一个线程能获取锁并修改内部数据T。- 原理:
Mutex<T>实现了Sync(前提是 T 是 Send),因为它通过锁机制序列化了对T的访问。
rust
编辑
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let mut handles = vec![];
for i in 0..3 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
// lock() 返回一个 MutexGuard
// 只有拿到 guard 才能修改数据
// 如果其他线程持有锁,这里会阻塞
let mut num = data_clone.lock().unwrap();
num.push(i);
// guard 离开作用域,锁自动释放
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
println!("{:?}", *data.lock().unwrap());
}
C. 原子操作 (Atomics)
对于简单的计数器或标志位,使用 std::sync::atomic 模块下的类型(如 AtomicUsize, AtomicBool)。
- 特点:无锁(Lock-free),性能比
Mutex高,但功能有限(只能做简单的读改写操作)。 - 内存序:需要理解
Ordering(Relaxed, Acquire, Release, SeqCst) 来保证指令重排的正确性。
rust
编辑
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let val = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..10 {
let val_clone = Arc::clone(&val);
handles.push(thread::spawn(move || {
// 原子增加,无需显式锁
val_clone.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles { h.join().unwrap(); }
println!("结果: {}", val.load(Ordering::SeqCst));
}
3. 内部可变性 (Interior Mutability) 的线程安全版本
单线程中我们常用 Cell 和 RefCell 实现内部可变性,但它们不是线程安全的。
多线程中对应的替代品是:
Cell-> 无直接对应 (通常用 Atomcis 或 Mutex)RefCell->Mutex或RwLockRc->Arc
表格
| 单线程类型 | 多线程安全替代品 | 说明 |
|---|---|---|
Rc<T> | Arc<T> | 原子引用计数 |
RefCell<T> | Mutex<T> / RwLock<T> | 运行时锁检查 vs 编译时借用检查 |
Cell<T> | AtomicT | 仅限基本类型的原子操作 |
4. 常见陷阱与编译器保护
陷阱 1: 误用 Rc 在线程间
rust
编辑
// 错误示例
use std::rc::Rc;
use std::thread;
let rc = Rc::new(5);
thread::spawn(move || { // 编译错误!
println!("{}", rc);
});
// 报错信息大致为:Rc<i32> cannot be sent between threads safely
// 原因:Rc 没有实现 Send trait
陷阱 2: 死锁 (Deadlock)
Rust 的编译器无法检测死锁。如果你嵌套获取锁且顺序不一致,程序会挂起。
- 解决:始终按固定顺序获取锁,或使用
try_lock()。
陷阱 3: 锁中毒 (Poisoning)
如果一个线程在持有 Mutex 锁时 Panic 了,锁会被“毒化”(Poisoned)。其他尝试获取该锁的线程不会阻塞,而是立即收到一个 Err(PoisonError)。
- 目的:防止其他线程在数据处于不一致状态下继续操作。
- 处理:可以使用
lock().unwrap_or_else(|e| e.into_inner())来恢复锁并尝试修复数据。
总结
Rust 的线程安全模型可以概括为:
- 默认隔离:线程间内存默认不共享,通过
move转移所有权。 - 显式共享:若要共享,必须使用实现了
Sync的类型。 - 可变共享:若要可变共享,必须加锁 (
Mutex,RwLock) 或使用原子类型 (Atomic)。 - 编译期保证:只要你通过了编译器的检查(特别是
Send和Sync约束),就绝对不会发生数据竞争(Data Race)。
这种设计虽然增加了初学者的认知负担,但它将原本需要在运行时测试甚至生产环境才能发现的并发 Bug,提前到了编写代码的阶段。