1. Rust线程安全的核心:Send与Sync的本质
当你第一次在Rust中尝试跨线程传递Rc<i32>时,编译器会毫不留情地抛出错误:
rust复制error[E0277]: `Rc<i32>` cannot be sent between threads safely
这个错误背后隐藏着Rust线程安全模型的核心机制——Send和Sync这两个标记trait。与大多数语言特性不同,它们没有实际的方法实现,而是作为编译器的"安全检查点"存在。理解它们的关键不在于记忆定义,而在于明白编译器究竟在防止什么灾难发生。
1.1 Send:所有权的跨线程迁移许可
Send trait的定义非常简单:如果一个类型实现了Send,那么这个类型的值可以安全地跨线程转移所有权。但这句话的实际含义需要拆解:
- 所有权转移:在Rust中,
move关键字会将值的所有权从一个作用域转移到另一个作用域。跨线程时,这个转移必须保证安全 - 安全标准:转移后,原线程不再访问该值,且新线程能正确管理其生命周期
让我们用Rc<T>和Arc<T>的对比来说明。Rc(引用计数指针)不是Send的,而Arc(原子引用计数指针)是。原因在于它们的内部实现:
rust复制// Rc的内部结构简化表示
struct RcBox<T> {
strong: usize, // 普通整数计数器
value: T,
}
// Arc的内部结构简化表示
struct ArcInner<T> {
strong: AtomicUsize, // 原子整数计数器
value: T,
}
Rc使用普通usize作为计数器,当两个线程同时修改时:
- 线程A读取计数器值为1
- 线程B同时读取计数器值也为1
- 两者都加1后写回,结果都是2(实际应该是3)
这种竞态条件会导致内存安全问题。而Arc使用AtomicUsize,其fetch_add操作是原子的,保证了计数器的正确性。
关键理解:
Send不是关于"能否"转移,而是关于转移后"是否安全"。编译器阻止的是可能引发内存损坏的操作。
1.2 Sync:共享引用的线程安全保证
如果说Send管理所有权的转移,那么Sync则管理共享引用的安全性。官方定义是:T: Sync当且仅当&T: Send。这意味着多个线程可以同时安全地持有对T的不可变引用。
理解这一点需要区分几种情况:
- 不可变共享:基本类型如
i32是Sync的,因为同时读取不会产生问题 - 内部可变性:
Cell<T>和RefCell<T>不是Sync的,因为它们允许通过共享引用修改数据 - 同步原语:
Mutex<T>和RwLock<T>是Sync的,即使T本身不是
`Cel
