定理:在 .await
执行期间,任务可能会在线程间转移
1.引例
Rust
//正确代码
async fn increment_and_do_stuff(mutex: &Mutex<i32>) {
{
let mut lock: MutexGuard<i32> = mutex.lock().unwrap();
*lock += 1;
} // lock在这里超出作用域 (被释放)
do_something_async().await;
}
//错误代码
async fn increment_and_do_stuff(mutex: &Mutex<i32>) {
let mut lock: MutexGuard<i32> = mutex.lock().unwrap();
*lock += 1; drop(lock);
do_something_async().await;
}
MutexGuard不是线程安全的,因为其没有实现Send 的trait,所以不能将Mutex锁在线程间传递。而在 .await
执行期间,任务可能会在线程间转移,所以在.wait执行之前lock被drop掉才对。
2.在跨线程场景中,实现Send和Sync是线程安全的必要条件
接口约束(Send
/Sync
) + 正确实现 + 所有权规则 = 真正的线程安全
我们提到了要保证线程安全至少要实现Send这个trait,事实上一个类型要真正线程安全,需要同时满足:
Send
:可以安全地在线程间转移所有权Sync
:可以安全地在多个线程间共享引用 (&T
) 只有同时实现了转移所有权 和共享引用 才能保证线程安全,否则会导致数据竞争 或悬垂指针。
四个组合
2.1实现了Send没实现Sync=>数据竞争
现状:可以安全转移所有权 ,无法安全共享引用
Rust
struct MyData {
value: i32;
}
// 错误地手动实现 Sync(不安全!)
unsafe impl Sync for MyData {}
fn main() {
let data = Arc::new(MyData { value: 42 });
let t1 = std::thread::spawn({
let d = Arc::clone(&data);
move || {
d.value += 1; // 修改数据
}
});
let t2 = std::thread::spawn({
let d = Arc::clone(&data);
move || {
d.value += 1; // 同时修改数据
}
});
t1.join().unwrap();
t2.join().unwrap();
}
这段代码可能会发生这种情况:
Rust
线程1: LOAD → 得到42
线程2: LOAD → 得到42 (在线程1存储前)
线程1: ADD → 43
线程1: STORE → 内存变为43
线程2: ADD → 43
线程2: STORE → 内存变为43
事实上Rust编译器是不会允许这个操作通过编译,报出以下错误 大意是
Arc
只提供不可变访问,我们无法修改其内部数据。
我们没有用互斥锁保护结构体内部的数据,导致可以同时获取同一块地址对其进行操作,这是不安全的。通过错误实现 Sync
,我们承诺 MyData
可以安全共享,但实际上,它不能安全地并发修改,这违反了 Rust 的安全约定。
我们修改后的代码为:
Rust
use std::sync::{Arc, Mutex};
struct MyData {
value: Mutex<i32>,
}
unsafe impl Sync for MyData {}
fn main() {
let data = Arc::new(MyData {
value: Mutex::new(42)
});
let mut handles = vec![];
let data_clone1 = Arc::clone(&data);
let handle1 = std::thread::spawn(move || {
let mut guard = data_clone1.value.lock().unwrap();
*guard += 1;
println!("线程1增加后: {}", *guard);
});
handles.push(handle1);
let data_clone2 = Arc::clone(&data);
let handle2 = std::thread::spawn(move || {
let mut guard = data_clone2.value.lock().unwrap();
*guard += 1;
println!("线程2增加后: {}", *guard);
});
handles.push(handle2);
for handle in handles {
handle.join().unwrap();
}
let final_value = data.value.lock().unwrap();
println!("最终值: {}", *final_value);
}

这样就确保每次只有一个线程访问数据。
2.2实现了Sync没实现Send=>悬垂引用
现状:可以安全共享引用 ,无法安全转移所有权
Rust
struct MyRef {
ptr: *const i32, // 裸指针
// 没有手动实现Sync,但 *const i32 实现了 Sync, 所以 MyRef 自动实现 Sync
}
unsafe impl Send for MyData {}
let ref_data;
{
let local_data = 42;
ref_data = MyRef { ptr: &local_data };
}
std::thread::spawn(move || {
println!("{}", unsafe { *ref_data.ptr }); // 未定义行为
});
同样,这个代码也是无法编译通过的,很简单,local_data
在离开作用域时被销毁,而ref_data
在构造时借用了local_data
,我们无法把ref_data
的值在线程之间传递。、 要想在线程之间传递我们需要这样修改:
Rust
use std::sync::Arc;
struct SafeRef {
data: Arc<i32>, // 引用计数指针
}
// 自动实现 Send 和 Sync
let safe_ref = SafeRef { data: Arc::new(42) };
std::thread::spawn(move || {
println!("{}", *safe_ref.data); // 安全
});
3.自动 trait 推导机制
3.1. Send
trait 的自动推导
- 规则 :当类型的所有字段都实现
Send
时,该类型自动实现Send
- 推导过程 :
i32
实现了Send
Arc<i32>
实现了Send
(因为i32: Send + Sync
)- 因此
SafeRef
自动实现Send
3.2. Sync
trait 的自动推导
- 规则 :当类型的所有字段都实现
Sync
时,该类型自动实现Sync
- 推导过程 :
i32
实现了Sync
Arc<i32>
实现了Sync
(因为i32: Send + Sync
)- 因此
SafeRef
自动实现Sync
4.并非实现了Send和Sync就一定能通过编译,或是实现安全的共享数据,因为还要满足所有权规则。
总结:Rust通过三位一体的机制保障线程安全:
-
Send
和Sync
trait:定义类型的基本线程安全属性 - 所有权系统:在编译期强制执行内存安全规则
- 借用检查器:防止数据竞争和悬垂指针
(本人新手一枚,如果有错误,希望各位大佬批评指正,谢谢)