Rust 如何保证线程安全?Send 和 Sync trait 的作用是什么?(详解编译期并发安全机制)

在并发编程领域,数据竞争(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)”。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
小码农的头像小码农认证作者

相关推荐

返回顶部