[Rust]由.await引起的对Send和Sync的思考

定理:.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通过三位一体的机制保障线程安全:

  1. SendSync trait​:定义类型的基本线程安全属性
  2. ​所有权系统​:在编译期强制执行内存安全规则
  3. ​借用检查器​:防止数据竞争和悬垂指针

(本人新手一枚,如果有错误,希望各位大佬批评指正,谢谢)

相关推荐
天天摸鱼的java工程师4 分钟前
QPS 10 万,任务接口耗时 100ms,线程池如何优化?
java·后端·面试
双向334 分钟前
从O(n²)到O(n log n):深度剖析快速排序的内存优化与cache-friendly实现
后端
回家路上绕了弯6 分钟前
深度解析:频繁 Full GC 的诊断与根治方案
jvm·后端
武子康8 分钟前
大数据-57 Kafka 高级特性 Producer 消息发送流程与核心配置详解
大数据·后端·kafka
知其然亦知其所以然9 分钟前
MySQL社招面试题:索引有哪几种类型?我讲给你听的不只是答案!
后端·mysql·面试
天天摸鱼的java工程师12 分钟前
掘金图片上传被拒:一次由CheckAuthenticationError引发的密钥‘失踪’迷案
java·后端
福大大架构师每日一题13 分钟前
2025-08-01:粉刷房子Ⅳ。用go语言,给定一个偶数个房屋排列在一条直线上,和一个大小为 n x 3 的二维数组 cost,其中 cost[i][j] 表
后端
error_cn14 分钟前
网络i_o对cpu负载分析
后端
bug菌15 分钟前
学生信息管理系统,真的是码农的必修课吗?
java·后端·java ee
就是帅我不改16 分钟前
深入实战建造者模式:在订单系统中的应用
后端·架构