在许多语言中(如 C++ 或 Java),多线程编程往往伴随着臭名昭著的数据竞争(Data Race)、死锁和难以排查的内存损坏。而 Rust 通过其独特的类型系统,将并发安全直接交给了编译器 在编译期进行静态检查,其中最核心的守护神就是两个特殊的 Marker Trait(标记特征):Send 和 Sync。
一、 核心概念解析:Send 与 Sync ------ 编译器如何守护并发安全?
1. 什么是 Send?(所有权跨线程转移)
- 定义 :如果一个类型实现了
Send,意味着该类型的所有权可以安全地从一个线程转移(Move)到另一个线程。 - 背后的哲学 :大多数 Rust 类型默认都是
Send的(如基础类型i32、f64、由Send类型组成的结构体等)。 - 典型反例 :
Rc<T>(单线程引用计数指针)。Rc内部的引用计数是普通整数增减(非原子操作),如果在多线程间转移并同时修改,会导致计数器紊乱和未定义行为(UB)。因此,Rc明确没有 实现Send。
代码对比:Rc vs Arc 的线程间转移
rust
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
fn send_demo() {
// ❌ 错误示例:Rc<T> 没有实现 Send
let rc_val = Rc::new(42);
// thread::spawn(move || {
// println!("{}", rc_val);
// // 编译报错: `Rc<i32>` cannot be sent between threads safely
// });
// ✅ 正确示例:Arc<T> 实现了 Send
let arc_val = Arc::new(42);
let handle = thread::spawn(move || {
println!("线程内接收到 Arc 数据: {}", arc_val);
});
handle.join().unwrap();
}
2. 什么是 Sync?(引用跨线程共享)
- 定义 :如果一个类型实现了
Sync,意味着多个线程同时通过不可变引用(&T)访问该类型是安全的。 - 数学等价关系 :
T 是 Sync ⟺ &T 是 Send\text{T 是 Sync} \iff \text{\&T 是 Send}T 是 Sync⟺&T 是 Send
也就是说,如果类型T的引用可以安全地发送到另一个线程,那么T就是Sync的。 - 典型反例 :
RefCell<T>。它提供内部可变性(Interior Mutability),但其借用检查是 非线程安全(Non-atomic) 的运行时检查。如果多个线程同时获取RefCell的可变借用,就会引发数据竞争。因此RefCell既不是Send也不是Sync。若要在多线程下实现内部可变性,应当使用Mutex<T>或RwLock<T>。
代码对比:RefCell vs Mutex 的多线程共享
rust
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
use std::thread;
fn sync_demo() {
// ❌ 错误示例:RefCell 不是 Sync,无法通过 &RefCell 跨线程共享
// let cell_data = Arc::new(RefCell::new(0));
// let cell_clone = Arc::clone(&cell_data);
// thread::spawn(move || {
// *cell_clone.borrow_mut() += 1;
// // 编译报错: `RefCell<i32>` cannot be shared between threads safely
// });
// ✅ 正确示例:Mutex 实现了 Sync(前提是内部类型满足 Send)
let counter = Arc::new(Mutex::new(0));
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut guard = counter_clone.lock().unwrap();
*guard += 1;
});
handle.join().unwrap();
println!("最终计数: {}", *counter.lock().unwrap());
}
3. 自动派生与自定义类型的线程安全性
这两个 Trait 都是自动特征(Auto Traits) :
只要一个复合类型(Struct / Enum)的所有成员都实现了 Send(或 Sync),编译器就会自动 为该复合类型派生 Send(或 Sync)。
rust
// 自动继承 Send 和 Sync
struct Point {
x: f64,
y: f64,
}
// 包含非 Send/Sync 字段时,整个结构体自动失去 Send/Sync
struct BadContainer {
data: std::rc::Rc<i32>, // Rc 使得 BadContainer 既不是 Send 也不是 Sync
}
二、 💡 实战应用与常见陷阱
陷阱 1:Async Rust 中跨 .await 点持有非 Send 类型
在编写 Tokio 或 async-std 等多线程异步运行时代码时,异步任务(Task)通常必须满足 Send。如果在 .await 暂停点之前持有了一个非 Send 变量,生成的 Future 就会失去 Send 特性,导致无法在多线程 Executor 中调度。
rust
// ❌ 陷阱示例:在 .await 跨越期间持有 RefCell / Rc
async fn bad_async_task() {
let non_send_data = std::cell::RefCell::new(100);
// 如果在 await 点持有 RefCell 的借用,异步任务将不是 Send
async_operation().await;
println!("{}", non_send_data.borrow());
}
// ✅ 优化策略:缩小非 Send 变量的作用域,或者改用线程安全的异步并发原语
async fn good_async_task() {
{
let non_send_data = std::cell::RefCell::new(100);
*non_send_data.borrow_mut() += 10;
// non_send_data 在这里离开作用域并被 Drop
}
// 此时跨越 await 点不携带非 Send 变量
async_operation().await;
}
async fn async_operation() {
// 模拟异步操作
}
陷阱 2:MutexGuard 的生命周期与死锁预防
Mutex<T> 的锁通过 RAII 机制管理,lock() 返回的 MutexGuard 会在离开作用域时自动释放锁。但在复杂逻辑中,不小心延长 MutexGuard 的生命周期容易引发死锁或降低并发吞吐量。
rust
use std::sync::{Arc, Mutex};
fn lock_scope_demo() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
// ❌ 不推荐:锁在整个 if 块中一直被持有
if data.lock().unwrap().contains(&2) {
// 在该大括号结束前,MutexGuard 一直存在!
// 如果这里再次获取相同的锁或耗时较长,会导致死锁或性能瓶颈
}
// ✅ 推荐:利用显式作用域或局部变量快速释放锁
let has_two = {
let guard = data.lock().unwrap();
guard.contains(&2)
// guard 在这里 Drop,提前释放锁
};
if has_two {
println!("包含元素 2");
}
}