跳到主要内容

Send 与 Sync

问题

SendSync 是什么?它们如何保证线程安全?

答案

定义

// 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

类型SendSync原因
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: SendSync 为什么是 unsafe auto trait

答案

  • auto:编译器根据字段自动推导,不需要手动实现
  • unsafe:手动实现需要 unsafe impl,因为错误的实现会导致内存不安全

这种设计让大多数类型自动正确,只有在编写底层 unsafe 代码时才需要手动处理。

相关链接