Send 与 Sync
问题
Send 和 Sync 是什么?它们如何保证线程安全?
答案
定义
// Send:可以安全地将所有权从一个线程转移到另一个线程
pub unsafe auto trait Send {}
// Sync:可以安全地从多个线程通过共享引用访问
// T: Sync 意味着 &T: Send
pub unsafe auto trait Sync {}
| Trait | 含义 | 等价表述 |
|---|---|---|
Send | 值可以跨线程移动 | T 可以 move 到另一个线程 |
Sync | 引用可以跨线程共享 | &T 可以发送到另一个线程 |
自动实现规则
如果一个类型的所有字段都是 Send/Sync,则该类型自动实现 Send/Sync。
// ✅ 自动 Send + Sync
struct User {
name: String, // String: Send + Sync
age: u32, // u32: Send + Sync
}
// ❌ 不是 Send + Sync(因为 Rc 不是)
struct BadShared {
data: Rc<String>, // Rc: !Send + !Sync
}
常见类型的 Send/Sync
| 类型 | Send | Sync | 原因 |
|---|---|---|---|
i32, String, Vec<T> | ✅ | ✅ | 纯值类型 |
Arc<T> | ✅(T: Send+Sync) | ✅ | 线程安全引用计数 |
Mutex<T> | ✅(T: Send) | ✅ | 互斥锁保护 |
Rc<T> | ❌ | ❌ | 引用计数非原子操作 |
Cell<T> | ✅ | ❌ | 内部可变性非线程安全 |
RefCell<T> | ✅ | ❌ | 运行时借用检查非线程安全 |
*const T / *mut T | ❌ | ❌ | 裸指针无安全保证 |
MutexGuard<T> | ❌ | ✅ | 不能跨线程 unlock |
编译器如何使用 Send/Sync
use std::rc::Rc;
fn main() {
let rc = Rc::new(42);
// ❌ 编译错误:Rc<i32> 不满足 Send
// thread::spawn(move || {
// println!("{}", rc);
// });
// ✅ 改用 Arc
let arc = std::sync::Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Arc<i32>: Send ✅
});
}
手动实现(unsafe)
在 unsafe 代码中,如果你能保证安全性,可以手动实现:
struct MyType {
ptr: *mut u8,
}
// ⚠️ 必须确保跨线程使用安全
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}
危险
手动实现 Send/Sync 必须极其谨慎,错误实现会导致数据竞争(未定义行为)。只有在确信安全性时才应该这样做。
常见面试问题
Q1: 为什么 Rc 不是 Send?
答案:
Rc 的引用计数使用普通(非原子)整数操作。如果两个线程同时 clone/drop Rc,会发生计数竞争导致 use-after-free 或内存泄漏。线程安全版本是 Arc(使用原子操作计数)。
Q2: T: Sync 和 &T: Send 是什么关系?
答案:
它们是等价的。Sync 的定义就是"共享引用可以安全发送到其他线程":
// T: Sync ↔ &T: Send
// 直觉:如果 &T 可以被多个线程访问,那 T 就是 Sync 的
Q3: Mutex<T> 为什么只要求 T: Send 而不是 T: Sync?
答案:
因为 Mutex 保证同一时刻只有一个线程访问内部数据(互斥),不存在同时共享访问的情况。所以内部数据只需要能跨线程转移(Send),不需要能同时共享(Sync)。
Mutex<T> 本身是 Sync 的——因为多个线程可以安全地持有 &Mutex<T> 并竞争锁。
Q4: Send 和 Sync 为什么是 unsafe auto trait?
答案:
auto:编译器根据字段自动推导,不需要手动实现unsafe:手动实现需要unsafe impl,因为错误的实现会导致内存不安全
这种设计让大多数类型自动正确,只有在编写底层 unsafe 代码时才需要手动处理。