Rust并发编程中的 Sync 和 Send Trait 详解
在并发编程中,数据竞争是最常见的安全隐患,即多个线程同时对同一变量进行读写。然而,当你在 Safe Rust 中编写可能产生数据竞争的代码时,编译器会直接拒绝编译。这背后的机制正是 Sync 和 Send 这两个标记 trait。
Send trait 表示类型的所有权可以在线程间转移,而 Sync trait 表示类型的引用可以在线程间共享。它们都没有方法声明或方法体,仅作为类型约束的标记信息,帮助编译器识别线程不安全的代码。
定义如下:
pub unsafe auto trait Send { }
pub unsafe auto trait Sync { }
本文深入探讨 Sync 和 Send trait,解释为何某些类型实现它们而另一些不实现,并讨论 Rust 并发编程的最佳实践。
Sync Trait
Sync trait 表明类型可以被多个线程同时安全地访问,这里的"访问"指的是只读共享。Rust 中几乎所有原始类型都实现了 Sync。
例如:
let x = 5; // i32 实现了 Sync
i32 类型实现了 Sync,因此在线程间共享 i32 值是安全的。
另一方面,提供内部可变性的类型(即在拥有不可变引用时仍能获取可变引用修改数据),如 Mutex<T>,当 T 未实现 Sync 时,该类型本身也不安全。
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
因为 Mutex 使用锁保护内部数据,若多个线程同时访问,可能导致数据竞争或死锁。
例如:
use std::sync::Mutex;
let m = Mutex::new(5); // Mutex<i32> 未实现 Sync
Mutex<i32> 未实现 Sync,因此跨线程共享是不安全的。要安全访问非 Sync 类型,必须使用适当的同步操作,如获取锁、执行操作和释放锁。
实现 Sync 的类型
Rust 中的 Sync trait 确保同一数据的多个引用(可变或不可变)可以安全地从多个线程并发访问。实现 Sync 的类型 T 被视为"线程安全"。
常见的 Sync 类型示例:
- 原始类型:
i32、bool、char等。 - 简单聚合类型:元组
(i32, bool)。 - 原子类型:
AtomicBool。
而非同步类型不能同时使用多个引用,例如:
Mutex<i32>— 访问内部 i32 前需锁定互斥锁。RefCell<i32>— 访问内部值前需借用 RefCell。Rc<i32>— 共享内部 i32 所有权,多可变借用不安全。
非 Sync 类型的多线程访问
Mutex
要安全访问非同步类型,使用互斥锁等同步原语。例如,使用作用域线程(crossbeam)结合 Mutex:
use std::sync::Mutex;
use crossbeam::scope;
let data = Mutex::new(String::from("hello"));
scope(|s| {
for _ in 0..3 {
s.spawn(|_| {
let mut val = data.lock().unwrap();
val.push_str(" world");
});
}
}).unwrap();
println!("{}", data.lock().unwrap()); // 输出类似 "hello world world world"
这里,Mutex 保护内部字符串,lock() 获取锁以阻止其他线程访问。
Atomic
原子类型如 AtomicU64 可用原子操作(如 fetch_add)安全访问:
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
let counter = AtomicU64::new(0);
let mut handles = vec![];
for _ in 0..10 {
let c = &counter;
handles.push(thread::spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", counter.load(Ordering::SeqCst)); // 输出 10
总结
在 Rust 中跨线程共享数据,数据必须:
- 类型为
Sync(原始/不可变类型)。 - 封装在互斥或原子类型中(
Mutex、RwLock、Atomic*)。 - 使用通道等消息传递技术传递所有权。
Send Trait
Send trait 表示类型可以安全地跨线程边界传输,即类型值的所有权可在线程间转移。原始类型如 i32 和 bool 是 Send,因为它们在线程间共享时没有内部引用或可变问题。
例如:
use std::thread;
let x = 42;
let handle = thread::spawn(move || {
println!("{}", x); // 安全,i32 实现了 Send
});
handle.join().unwrap();
但 Rc<i32> 未实现 Send,因为其引用计数内部可变,多线程同时修改会导致内存不安全:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(5);
let handle = thread::spawn(move || {
println!("{}", rc); // 编译错误:Rc<i32> 未实现 Send
});
非 Send 类型如 Rc<T> 只能单线程使用,但可包装在 Arc<T> 等线程安全包装器中,Arc 使用原子操作管理引用计数,允许内部类型在线程间共享。
Send 的关键点:
Send类型可在线程间转移所有权。- 原始类型是
Send。 - 具有内部可变性的类型(如
Rc<T>)通常不是Send。 - 非
Send类型可在单线程中使用,或包装在Arc中共享。 - 跨线程传输非
Send类型会导致未定义行为。
自定义实现 Sync 和 Send
要创建自定义 Sync 或 Send 类型,需实现相应 trait。例如,结构体 MyBox 持有裸指针 *const u8,由于裸指针未实现 Send 和 Sync,因此 MyBox 默认也不是 Send 或 Sync。
若手动实现 Send 和 Sync,则可通过 Arc 在线程间传递和共享数据。但建议谨慎实现,因为需要确保线程安全性。
某些类型不可能实现 Sync 和 Send,例如 Rc<T> 不能设为 Send(引用计数需原子更新),RefCell<T> 不能设为 Sync(借用检查非线程安全)。
规则与最佳实践
理解 Sync/Send 与混合类型的规则至关重要:
- 类型必须是
Send才能在线程间移动。 - 若类型包含非
Send类型,则外部类型不能是Send(如Option<Rc<i32>>)。 Sync类型可通过共享引用并发使用;非Sync类型一次只能在一个线程中可变。- 若类型包含非
Sync类型,则外部类型不能是Sync(如Mutex<Rc<i32>>)。
并发 Rust 代码的最佳实践:
- 优先使用不可变数据结构。
- 需要修改时,使用
Mutex<T>等同步原语。 - 使用消息传递而非直接共享内存,避免数据竞争。
- 限制锁定范围,避免长时间持有锁影响性能。
- 根据是否实现
Sync和Send选择类型,如优先Arc而非Rc。 - 使用原子类型实现简单并发访问,避免加锁。