在并发编程领域,数据竞争(Data Race) 是导致系统崩溃和安全漏洞的“万恶之源”。传统语言(如 C++、Java)依赖程序员手动使用锁、原子操作等同步原语来避免竞争,但一旦疏忽,就会产生难以复现的幽灵 Bug。Rust 则采取了革命性的方法:将线程安全问题从运行时错误转变为编译期错误。这一能力的核心,正是两个特殊的 标记 trait(Marker Traits) —— Send 和 Sync。
核心概念:什么是 Send 和 Sync?
Send:跨线程所有权转移的安全许可
pub unsafe auto trait Send {}
- 语义:如果类型
T实现了Send,则T的所有权可以安全地在线程间转移(move)。 - 关键点:
- 转移的是整个值的所有权,而非共享引用。
- 转移后,原线程不再持有该值,因此不存在多线程同时访问的问题。
- 典型例子:
i32,String,Vec<T>(当T: Send时)都是Send。Rc<T>不是Send,因为其引用计数非原子操作,跨线程会导致计数错误。
Sync:跨线程共享引用的安全许可
pub unsafe auto trait Sync {}
- 语义:如果类型
T实现了Sync,则&T(不可变引用)可以安全地被多个线程同时访问。 - 关键点:
- 共享的是不可变引用,因此要求类型内部不能有可变状态,或可变状态已被内部同步机制(如
Mutex)保护。 Sync的正式定义是:T是Sync当且仅当&T是Send。
- 共享的是不可变引用,因此要求类型内部不能有可变状态,或可变状态已被内部同步机制(如
- 典型例子:
i32,String是Sync。Mutex<T>是Sync(当T: Send时),因为其内部锁保证了线程安全。Cell<T>不是Sync,因为它允许内部可变性且无同步机制。
Rust 如何利用它们保证线程安全?
Rust 的标准库中,所有涉及多线程的 API 都对泛型参数施加了 Send/Sync 约束:
1. 线程创建 (std::thread::spawn)
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
- 闭包
F必须是Send:因为闭包会被移动到新线程执行。 - 返回值
T必须是Send:因为结果需要从子线程传回主线程。
反例:尝试在线程间传递 Rc
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// 编译错误!`Rc<i32>` does not implement `Send`
thread::spawn(move || {
println!("{}", rc);
});
2. 共享状态 (Arc, Mutex)
use std::sync::{Arc, Mutex};
use std::thread;
let shared_data = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let data = Arc::clone(&shared_data);
// `Arc<Mutex<i32>>` is both `Send` and `Sync`
handles.push(thread::spawn(move || {
let mut num = data.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
Arc<T>是Send + Sync(当T: Send + Sync时),允许多个线程共享所有权。Mutex<T>是Send + Sync(当T: Send时),提供内部可变性和线程安全。
自动实现规则与例外
Rust 编译器会自动为大多数类型推导 Send 和 Sync,规则如下:
| 类型 | Send? |
Sync? |
原因 |
|---|---|---|---|
基本类型 (i32, bool) |
✅ | ✅ | 无内部指针,复制即安全 |
所有字段均为 Send 的结构体 |
✅ | 取决于字段 | 结构体安全当且仅当所有字段安全 |
Rc<T> |
❌ | ❌ | 引用计数非原子 |
Arc<T> |
✅ (if T: Send) |
✅ (if T: Sync) |
原子引用计数 |
Mutex<T> |
✅ (if T: Send) |
✅ (if T: Send) |
内部锁保证安全 |
裸指针 (*const T, *mut T) |
❌ | ❌ | 无所有权语义,可能悬空 |
💡 记忆口诀:
Send关注“移动”:值能否安全地交给另一个线程?Sync关注“共享”:多个线程同时读这个值是否安全?
手动实现(Unsafe)
对于通过 FFI 封装的外部库,若已知其线程安全,但内部包含裸指针(默认非 Send/Sync),可手动实现:
struct MyThreadSafeWrapper {
raw_ptr: *mut libc::some_c_struct,
}
// SAFETY: The underlying C library guarantees thread safety.
unsafe impl Send for MyThreadSafeWrapper {}
unsafe impl Sync for MyThreadSafeWrapper {}
⚠️ 警告:手动实现需绝对确保线程安全,否则会破坏 Rust 的内存安全保证。
总结:Rust 并发安全的哲学
- 零成本抽象:
Send/Sync是编译期检查,无运行时开销。 - 防御性设计:默认保守(如
Rc非Send),迫使开发者显式选择线程安全方案(如Arc)。 - 组合性:复杂类型的线程安全性由其组成部分自动推导,无需重复验证。
通过 Send 和 Sync,Rust 在不牺牲性能的前提下,将并发编程中最危险的数据竞争问题,在代码编译阶段就彻底消灭,真正实现了 “无畏并发(Fearless Concurrency)”。