Send 与 Sync
一句话理解
Send和Sync是 Rust 并发的类型级护栏,回答两个问题:
T: Send—— 这个值的所有权能不能交给另一个线程?T: Sync—— 这个值的共享引用&T能不能给多个线程同时用?它们是 auto trait:编译器会为复合类型自动推导,你几乎不用手写。它们的价值在于——编译期就把数据竞争挡在门外。
1. 两个标记的确切含义
// 标准库中的定义(简化)
pub unsafe auto trait Send {}
pub unsafe auto trait Sync {}| Trait | 含义 | 等价说法 |
|---|---|---|
Send | 可以把所有权移动到另一个线程 | T 可以跨线程传递 |
Sync | 多个线程可以同时持有 &T | &T: Send 当且仅当 T: Sync |
关系要记牢:
T: Sync ⟺ &T: Send换句话说,“能共享”就是”共享引用能跨线程送过去”。
2. 谁不是 Send / Sync
| 类型 | Send | Sync | 原因 |
|---|---|---|---|
Rc<T> | ❌ | ❌ | 引用计数是普通整数,非原子,并发加减会算错 |
Arc<T> | ✅(当 T: Send + Sync) | ✅(同左) | 原子计数 |
Cell<T> / RefCell<T> | ✅ | ❌ | 内部可变性无同步,多个 & 就能改值 |
Mutex<T> | ✅(当 T: Send) | ✅(当 T: Send) | 加锁保证互斥 |
RwLock<T> | ✅(当 T: Send) | ✅(当 T: Send + Sync) | 同上 |
*const T / *mut T | ❌ | ❌ | 裸指针无安全保证 |
&T | ✅(当 T: Sync) | ✅(当 T: Sync) | |
&mut T | ✅(当 T: Send) | ✅(当 T: Sync) | |
Vec<T> / Option<T> | 跟随 T | 跟随 T | 复合类型自动推导 |
复合类型是自动推导的
如果结构体的每个字段都是
Send,结构体就是Send。所以你几乎只在用Rc、裸指针、或者手写unsafe时才会撞到Send/Sync报错。
报错长这样:
error[E0277]: `Rc<String>` cannot be sent between threads safely
= help: within `[closure]`, the trait `Send` is not implemented for `Rc<String>`
note: required by a bound in `spawn`看到这个错的正确反应是换类型,而不是想办法绕过。
3. thread::spawn 的两个要求
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,Send:闭包要跨线程,它的所有捕获物必须能送过去'static:线程可能活得比创建它的作用域更久,所以不能借用局部变量
use std::thread;
let data = vec![1, 2, 3];
let handle = thread::spawn(move || { // move 把 data 的所有权转移进去
data.iter().sum::<i32>()
});
println!("{}", handle.join().unwrap());不想 move 所有权?用 scoped threads(Rust 1.63+),编译器会在作用域结束时自动 join:
let data = vec![1, 2, 3];
thread::scope(|s| {
s.spawn(|| {
println!("借用,不转移所有权: {:?}", &data); // ✅ 允许借用
});
}); // 这里保证所有线程已结束4. 两种并发模型
消息传递:用通信共享内存
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
for i in 0..3 {
let tx = tx.clone();
std::thread::spawn(move || {
tx.send(i * 10).unwrap();
});
}
drop(tx); // 关掉最后一个发送端,rx 的迭代才会结束
for value in rx {
println!("收到 {value}");
}Sender<T>: Send(当T: Send),可以 clone 出多个生产者Receiver<T>是Send但不是Sync:可以移动到别的线程,但不能多个线程共享同一个接收端
共享状态:加锁
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new(Vec::new()));
std::thread::scope(|s| {
for i in 0..3 {
let data = Arc::clone(&shared);
s.spawn(move || {
data.lock().unwrap().push(i);
});
}
});优先选消息传递
“不要通过共享内存来通信,而要通过通信来共享内存”。共享状态需要你同时推理锁的粒度、加锁顺序、锁中毒、跨 await 持锁四件事;消息传递只需要推理通道的生命周期。
5. Rust 挡住了什么,没挡住什么
挡住的是数据竞争,不是全部竞态
- 数据竞争(data race):多个线程同时访问同一内存、至少一个在写、且没有同步。Rust 用
Send/Sync在编译期彻底排除。- 竞态条件(race condition):逻辑上的时序依赖,比如”检查文件存在 → 创建文件”之间的 TOCTOU。Rust 不管这个,需要你自己用锁、原子操作或重试来保证。
还有两类它也不管:
- 死锁:两个线程按相反顺序拿两把锁
- 锁中毒导致的连锁 panic
6. 常见坑
std::sync::Mutex 不可重入
let m = Mutex::new(1);
let g = m.lock().unwrap();
// let g2 = m.lock().unwrap(); // ❌ 死锁,同一线程再次加锁会阻塞自己加锁顺序要统一
线程 A:lock(a) → lock(b)
线程 B:lock(b) → lock(a) ❌ 经典死锁约定一个全局的加锁顺序,或让临界区只涉及一把锁。
MutexGuard 不是 Send
std::sync::MutexGuard 不能跨线程移动。这直接导致一个常见报错:在 async 里跨 .await 持有 std::sync::MutexGuard 会编译失败(见 async 与 Tokio 入门)。
别在锁内做慢操作
锁内调用网络、磁盘、日志 IO 会把并发度打成串行。缩小临界区是并发优化的第一原则。
7. 手动实现 Send / Sync
只有在你确定额外的不变量成立时才用:
struct MyBox(*mut u8);
// ⚠️ 这是一个承诺:我保证这个类型跨线程使用是安全的
unsafe impl Send for MyBox {}
unsafe impl Sync for MyBox {}
unsafe impl是安全边界上的书面保证写错了会导致未定义行为,而且编译器不会再帮你。绝大多数业务代码一辈子不需要写这两行。真需要时,通常意味着应该用
Arc<Mutex<_>>、原子类型或既有的并发容器。
8. 数据并行:rayon
不需要手写线程池,把迭代器换成并行版即可:
use rayon::prelude::*;
let sum: i64 = (1..=1_000_000).into_par_iter().map(|x| x * x).sum();rayon 内部用工作窃取线程池,要求元素类型满足 Send(因为要分发到不同线程)。CPU 密集型任务用它,比手写 thread::spawn 简洁得多。
9. 小结
关键判断
Send= 所有权可跨线程;Sync= 共享引用可跨线程;T: Sync⟺&T: Send- 它们由编译器自动推导,看到报错就换类型,不要
unsafe implRc→Arc、RefCell→Mutex/RwLock,这是两条最标准的替换路径- Rust 保证没有数据竞争,但不保证没有死锁和逻辑竞态
- 优先消息传递;必须共享状态时,缩小临界区、统一加锁顺序