1、定义与语义
Send:表示类型的所有权可以在线程之间安全转移。
- 如果
T: Send,则可以把T的值move到另一个线程。 - 绝大多数基本类型(如
i32、bool、String)、标准库中的Vec<T>、Box<T>等都实现了Send。
Sync:表示类型的共享引用可以在多个线程之间安全访问。
T是Sync当且仅当&T: Send。- 如果一个类型
T实现了Sync,那么多个线程可以同时持有&T而不会引发数据竞争(即只读共享是安全的)。 - 典型的
Sync类型有i32、Mutex<T>(内部使用了同步原语)等。
一句话区分:
Send管的是**"把值交给另一个线程"**(转移所有权)。Sync管的是 **"把不可变引用同时给多个线程用"**(共享引用)。
2、 自动推导规则(auto trait)
编译器会按照结构体/枚举字段的组成 自动为自定义类型实现 Send 和 Sync:
- 一个结构体(或枚举)的所有字段都实现了
Send,则该结构体自动实现Send。 - 所有字段都实现了
Sync,则该结构体自动实现Sync。 - 泛型类型
Foo<P>的Send/Sync实现取决于其类型参数P是否实现了相应 trait。
因此,只要你的类型完全由线程安全的组件构成,它就自动具备线程安全属性。
3、 常见的 Send 与 Sync 典型案例
表格
| 类型 | Send | Sync | 说明 |
|---|---|---|---|
i32, f64, bool 等基础类型 |
✅ | ✅ | 原始类型天生线程安全 |
String, Vec<T>(T: Send) |
✅ | ✅ | 由 Send 部分组成 |
Box<T>(T: Send) |
✅ | ✅(若 T: Sync) |
参考实现依赖内部类型 |
Arc<T>(T: Send + Sync) |
✅ | ✅(若 T: Sync) |
原子引用计数,线程安全共享 |
Mutex<T>(T: Send) |
✅ | ✅ | 内部有锁,可安全共享可变访问 |
RwLock<T>(T: Send) |
✅ | ✅ | 读写锁,类似 Mutex |
mpsc::Sender<T>(T: Send) |
✅ | ❌ | 只能移动,不能多线程共享 |
Rc<T> |
❌ | ❌ | 非原子引用计数,多线程下计数更新会竞争 |
RefCell<T>, Cell<T> |
❌(单线程可用) | ❌ | 内部可变性无同步保护,跨线程共享引用可能导致数据竞争 |
*const T, *mut T |
❌ | ❌ | 裸指针不提供任何安全保证 |
可以看到,Rc<T> 和 RefCell<T> 是典型的非线程安全类型,因为它们的设计没有考虑并发场景。在需要跨线程的场景下,应该用 Arc<T> 替代 Rc<T>,用 Mutex<T> 或 RwLock<T> 替代 RefCell<T>。
4、 数值上的并发示例
(1)Send 示例:将数据 move 到新线程
rust
use std::thread;
fn main() {
let v = vec![1, 2, 3]; // Vec<i32> 实现了 Send
let handle = thread::spawn(move || { // 传递 v 的所有权给线程
println!("{:?}", v); // 所有权已移入线程
});
handle.join().unwrap(); // 等待线程完成
}
如果把 Vec 换成 Rc<T>,编译会报错:Rc 没有实现 Send,无法跨线程转移。
(2)Sync 示例:多线程共享只读数据
rust
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]); // Arc<Vec<i32>> 既 Send 又 Sync
let mut handles = vec![]; // 存储线程句柄的向量
for _ in 0..4 {
let shared = Arc::clone(&data); // 克隆 data,创建新的 Arc 实例
// 每个线程都拥有 data 的一个引用,所以可以安全地访问 data
handles.push(thread::spawn(move || { // 传递 shared 的所有权给线程
let sum: i32 = shared.iter().sum(); // 计算 shared 中所有元素的和
println!("sum = {}", sum);
}));
}
for h in handles { // 等待所有线程完成
h.join().unwrap(); // 等待线程完成
}
}
这里 Arc 允许多个线程共享同一份不可变数据,Vec<i32> 实现了 Sync,所以安全。
(3)需要可变共享时,配合 Mutex:
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u64)); // 共享的计数器
let mut handles = vec![]; // 存储线程句柄的向量
for _ in 0..8 {
let c = Arc::clone(&counter); // 克隆 counter,创建新的 Arc 实例
handles.push(thread::spawn(move || { // 传递 c 的所有权给线程
for _ in 0..100_000 {
*c.lock().unwrap() += 1; // 加锁后修改 counter 的值
}
}));
}
for h in handles { // 等待所有线程完成
h.join().unwrap(); // 等待线程完成
}
println!("count = {}", *counter.lock().unwrap()); // 打印 counter 的值
}
Mutex<T> 自身实现了 Sync,即使内部 T 不是 Sync,也能通过锁提供线程安全的内部可变性。
5、手动实现 Send / Sync 的注意事项
Send 和 Sync 是 unsafe trait ,这意味着手动实现它们需要用 unsafe impl,程序员必须自己保证相关的并发安全承诺:
- 错误地实现
Send可能导致一个线程在另一个线程仍在访问数据时将其释放,产生悬垂指针。 - 错误地实现
Sync可能允许多个线程同时对同一内存位置进行无同步的修改,引发数据竞争和未定义行为。
绝大多数情况下,你不需要也绝对不应轻易 手动实现这两个 trait。应该依赖编译器自动推导,或者使用已经正确封装好的标准库类型(如 Arc、Mutex、Atomic* 等)来组合出安全的并发结构。
rust
use std::ptr::NonNull; // 引入 NonNull 指针类型,用于存储非空指针
struct MyBox<T>(NonNull<T>); // 定义一个 MyBox 结构体,用于存储非空指针
unsafe impl<T> Send for MyBox<T> {} // 实现 Send 特征,允许 MyBox 在线程之间发送
unsafe impl<T> Sync for MyBox<T> {} // 实现 Sync 特征,允许 MyBox 在线程之间共享
impl<T> MyBox<T> {
fn new(x: *mut T) -> Option<Self> { // 创建一个 MyBox 实例
// 检查指针是否为空,如果为空则返回 None,否则返回 Some(MyBox) 实例
NonNull::new(x).map(|p| Self(p))
}
unsafe fn get(&self) -> &T { // 获取 MyBox 中存储的指针指向的值
unsafe { &*self.0.as_ptr() } // 从指针中获取引用
}
}
fn main() {
let x = MyBox::new(&mut 100 as *mut i32); // 创建一个 MyBox 实例,指向 100
// 检查 MyBox 是否为空
if let Some(ref my_box) = x { // 如果 MyBox 不为空,ref 关键字表示借用 MyBox 实例
// 从 MyBox 中获取引用
let y = unsafe { my_box.get() };
println!("{}", y);
} else {
println!("MyBox::new 返回了 None");
}
// 将 MyBox 实例 x 传递给线程内使用
let thread = std::thread::spawn(move || {
if let Some(my_box) = x {
let y = unsafe { my_box.get() };
println!("{}", y);
} else {
println!("MyBox::new 返回了 None");
}
});
// 等待线程执行完成
thread.join().unwrap();
}
6、总结
Send:类型所有权可跨线程传递,大部分类型自动实现;Rc、RefCell、裸指针除外。Sync:类型共享引用可跨线程安全访问;T: Sync↔&T: Send。- 编译器根据字段组成自动推导
Send/Sync,正确组合标准库中的线程安全容器即可满足绝大多数并发场景。 - 多线程共享数据的典型模式:
Arc<Mutex<T>>或Arc<RwLock<T>>;单线程通信用mpsc通道。 - 手动实现这两个 trait 属于
unsafe操作,一般只出现在编写底层同步原语或 FFI 封装时。
Rust 的 Send 和 Sync 将线程安全的保证从运行时提前到编译期,让"无畏并发"真正落地。