Rust Send 与 Sync:编译期的线程安全合同
更新时间:2026-09-20。Rust 不靠运行时加锁兜底,而是用两个**没有任何方法的标记 trait(auto trait)**在编译期描述「这个类型能不能跨线程」。线程 spawn 的约束见 std::thread,async 场景的 Send 见 async/Future 与 Send/Sync。
一、一句话区别
| trait | 含义 | 直觉 |
|---|---|---|
Send | 类型的所有权可以 move 给另一个线程 | 能「寄」过去 |
Sync | &T 能被多个线程同时持有而不出事 | 能「共享引用」 |
有个好记的等价关系:T: Sync 当且仅当 &T: Send——能把不可变引用安全寄到别的线程,就说明这个类型支持并发共享。
二、自动派生:大多数类型不用手写
编译器对 Send/Sync 采用「逐字段自动推导」:一个 struct 若所有字段都是 Send,它就是 Send;所有字段都 Sync,它就是 Sync。
rust
// 普通类型自动满足
let s = String::from("hi");
std::thread::spawn(move || println!("{s}")); // String: Send,OK
// Rc 不是 Send —— 编译直接拒绝
let rc = std::rc::Rc::new(5);
std::thread::spawn(move || println!("{rc}"));
// error: Rc<i32> cannot be sent between threads safely| 类型 | Send | Sync | 原因 |
|---|---|---|---|
i32/String/Vec/Box<T:Send> | ✅ | ✅ | 独占所有权、无共享可变 |
Arc<T>(T: Sync+Send) | ✅ | ✅(T: Sync) | 原子引用计数,共享只读 |
Mutex<T> | T: Send 即可 | ✅ | 运行期加锁,&self 也能 lock 改写 |
Rc<T> | ❌ | ❌ | 引用计数非原子,多线程会丢更新 |
RefCell<T> | ✅ | ❌ | 运行时借用计数非原子 |
裸指针 *const/*mut T | ❌ | ❌ | 编译器不替你保证 |
三、Arc 与 Mutex 为什么搭着用
Arc<T>解决共享所有权:原子计数允许多线程各持一份 clone,但它只给你共享不可变引用,要改内部得 T 本身内部可变。Mutex<T>解决共享可变性:靠锁把「同一时刻只有一个线程能拿到&mut T」,于是Mutex<T>本身是 Sync。
所以跨线程共享一个可变计数器的标准组合是 Arc<Mutex<i32>>,详见 Arc/Mutex/RwLock。
四、unsafe impl:我比编译器更清楚
极少数类型内部用了裸指针/锁但逻辑上线程安全(如 Mutex 自己),需要手动声明:
rust
struct MyWrapper(*const u8);
// SAFETY: 我保证内部指针的访问已自行同步
unsafe impl Send for MyWrapper {}
unsafe impl Sync for MyWrapper {}这是把安全责任从编译器转给你,99% 的业务代码不需要写,写错就是数据竞争/UB。
五、常见坑
- 把 Send/Sync 当普通 trait 去 impl 方法:它们是无方法的标记,auto trait 一般自动派生。
Rc/RefCell跨线程:编译报错,换Arc/Mutex/RwLock。- 以为
Arc<T>就能改 T:Arc 只共享只读,要改套Mutex/Atomic*。 - 闭包捕获了非 Send 类型:spawn 报
cannot be sent,顺着捕获列表找Rc/裸指针。 - 滥用
unsafe impl关报错:等于关掉编译器的并发保险,必须有充分论证。
总结
Send 表示所有权能安全转移到其他线程、Sync 表示 &T 能被多线程安全共享(等价于 &T: Send),二者是无方法的 auto trait,编译器按字段自动推导,所以普通类型天生满足,而 Rc(非原子计数)、RefCell(非原子借用)、裸指针被排除;Arc 以原子计数提供共享所有权但只给只读、Mutex/RwLock/原子类型提供共享可变性并因此是 Sync,跨线程可变共享的标准答案是 Arc<Mutex<T>>,只有确信内部已自行同步时才用 unsafe impl 手动担保。