Rust - Send & Sync

Rust 的 SendSync trait,这是 Rust 并发编程的核心机制,让 Rust 能在编译期防止数据竞争。

1. 为什么需要 Send 和 Sync?

想象你有一个玩具:

  • 问题1:你能把玩具给另一个城市的朋友吗?(跨线程传递)
  • 问题2:多个朋友能同时玩这个玩具吗?(跨线程共享)

在 Rust 中:

  • Send 解决第一个问题:能否安全转移所有权到其他线程
  • Sync 解决第二个问题:能否安全地在线程间共享引用

2. Send:安全传递的通行证

2.1 什么是 Send?

  • 表示类型可以安全地跨线程传递所有权
  • 就像玩具带有"可邮寄"标签,可以安全寄给朋友

2.2 哪些类型实现了 Send?

rust 复制代码
// 基本类型都是 Send
let a: i32 = 42;          // Send ✓
let b: String = "hello".to_string(); // Send ✓

// 标准库中的线程安全类型
let c: Mutex<i32> = Mutex::new(0); // Send ✓
let d: Arc<i32> = Arc::new(42);    // Send ✓

2.3 哪些类型不是 Send?

rust 复制代码
// Rc 不是线程安全的
use std::rc::Rc;
let e: Rc<i32> = Rc::new(42); // 不是 Send ✗

// 原始指针不是自动 Send
let f: *const i32 = &42 as *const i32; // 不是 Send ✗

2.4 实际意义

rust 复制代码
use std::thread;

// 正确示例:传递 String(实现了 Send)
thread::spawn(move || {
    let s = String::from("hello");
    println!("{}", s);
});

// 错误示例:尝试传递 Rc
let rc = Rc::new(42);
thread::spawn(move || {
    println!("{}", rc); // 编译错误:`Rc<i32>` cannot be sent between threads safely
});

3. Sync:安全共享的许可证

3.1 什么是 Sync?

  • 表示类型的不可变引用可以安全地在多个线程中使用
  • 就像玩具带有"可共享"标签,多个朋友可以同时看(但不能同时修改)

3.2 哪些类型实现了 Sync?

rust 复制代码
// 基本不可变类型
let a: &i32 = &42;          // Sync ✓

// 线程安全的包装类型
let b: Arc<i32> = Arc::new(42);     // Sync ✓
let c: Mutex<i32> = Mutex::new(0);  // Sync ✓

// 原子类型
use std::sync::atomic::AtomicI32;
let d: AtomicI32 = AtomicI32::new(0); // Sync ✓

3.3 哪些类型不是 Sync?

rust 复制代码
// Cell 和 RefCell 不是线程安全的
use std::cell::RefCell;
let e: RefCell<i32> = RefCell::new(42); // 不是 Sync ✗

// Rc 也不是 Sync
use std::rc::Rc;
let f: Rc<i32> = Rc::new(42); // 不是 Sync ✗

3.4 实际意义

rust 复制代码
use std::thread;
use std::sync::Arc;

// 正确示例:共享 Arc(实现了 Sync)
let data = Arc::new(42);
for _ in 0..5 {
    let data = Arc::clone(&data);
    thread::spawn(move || {
        println!("{}", data); // 多个线程同时读取
    });
}

// 错误示例:尝试共享 RefCell
let bad = RefCell::new(42);
thread::spawn(move || {
    println!("{}", bad.borrow()); // 编译错误:`RefCell<i32>` cannot be shared between threads
});

4. Send 和 Sync 的关系

4.1 核心关系表

类型 T 的特性 含义 常见例子
T: Send 可以安全移动到其他线程 i32, String, Mutex<T>
T: Sync 可以安全在线程间共享引用 i32, &str, Arc<T>
T: Send + Sync 既可移动又可共享 Arc<Mutex<T>>, AtomicI32
!Send 不能安全移动到其他线程 Rc<T>, 原始指针
!Sync 不能安全共享引用 Cell<T>, RefCell<T>

4.2 重要规则

  1. 自动派生规则

    • 如果类型的所有字段都是 Send,那么该类型也是 Send
    • 如果类型的所有字段都是 Sync,那么该类型也是 Sync
  2. 引用规则

    • &TSend 当且仅当 T: Sync
    • &mut TSend 当且仅当 T: Send
  3. 智能指针规则

    • Box<T>: Send 当且仅当 T: Send
    • Arc<T>: SendSync 当且仅当 T: Send + Sync

5. 深入原理:编译器如何保证安全

5.1 编译期检查

Rust 编译器在编译时检查:

rust 复制代码
fn spawn_thread<T: Send>(data: T) {
    thread::spawn(move || {
        use_data(data);
    });
}
  • 这里 T: Send 约束确保只有可安全传递的类型才能使用

5.2 错误示例分析

rust 复制代码
use std::rc::Rc;

let rc = Rc::new(42);
thread::spawn(move || {
    println!("{}", rc);
});

编译器会报错:

rust 复制代码
error[E0277]: `Rc<i32>` cannot be sent between threads safely
   --> src/main.rs:10:5
    |
10  |     thread::spawn(move || {
    |     ^^^^^^^^^^^^^ `Rc<i32>` cannot be sent between threads safely
    |
    = help: the trait `Send` is not implemented for `Rc<i32>`

5.3 为什么 Rc 不是 Send/Sync?

  • Rc 使用非原子引用计数
  • 如果多个线程同时修改计数,会导致计数错误
  • Arc 使用原子操作,所以是线程安全的

6. 实际应用模式

模式1:构建自定义线程安全类型

rust 复制代码
use std::sync::{Arc, Mutex};

struct SafeCounter {
    count: Mutex<i32>,
}

impl SafeCounter {
    fn new() -> Self {
        SafeCounter { count: Mutex::new(0) }
    }
    
    fn increment(&self) {
        let mut lock = self.count.lock().unwrap();
        *lock += 1;
    }
}

// 自动实现 Send + Sync
unsafe impl Send for SafeCounter {} // 安全:因为内部类型都是线程安全的
unsafe impl Sync for SafeCounter {} // 安全:因为内部锁保证安全

模式2:线程隔离数据

rust 复制代码
// 故意使用 !Send 类型来限制数据在单线程
struct GuiContext {
    widgets: Vec<Rc<Widget>>, // Rc 不是 Send
}

impl GuiContext {
    // 确保只在创建线程使用
    fn update(&mut self) {
        // GUI更新操作
    }
}

// 明确标记为 !Send
impl !Send for GuiContext {}

模式3:跨线程通信

rust 复制代码
use std::sync::mpsc;
use std::thread;

// 通道两端自动实现 Send
let (sender, receiver) = mpsc::channel();

thread::spawn(move || {
    sender.send("hello from thread").unwrap();
});

println!("{}", receiver.recv().unwrap());

7. 常见问题解答

Q: 为什么 Mutex 既是 Send 又是 Sync? A:

  • Send:因为可以安全移动到其他线程
  • Sync:因为多个线程可以共享 &Mutex(通过锁机制保证安全访问)

Q: 为什么 RefCell 不是 Sync? A: 因为它的借用检查在运行时进行,不是线程安全的。多个线程同时调用 borrow_mut() 可能导致数据竞争。

Q: 如何让自定义类型实现 Send/Sync? A: 大多数情况下编译器会自动实现。如果需要手动实现:

rust 复制代码
unsafe impl Send for MyType {} // 确保没有非Send字段
unsafe impl Sync for MyType {} // 确保没有非Sync字段

但需要非常小心,确保真正满足线程安全要求。

Q: 遇到 "trait bound not satisfied" 错误怎么办? A: 通常是因为:

  1. 尝试在线程间传递非 Send 类型 → 改用 Arc/Mutex
  2. 尝试在线程间共享非 Sync 类型 → 使用原子类型或锁
相关推荐
该用户已不存在14 小时前
OpenAI 用 Rust 重写 AI 工具,你的本地开发环境跟上了吗?
前端·javascript·rust
火柴就是我15 小时前
Rust 学习只 字符串操作知识点
rust
维维酱17 小时前
一些基础概念
rust
寻月隐君18 小时前
Rust + Protobuf:从零打造高效键值存储项目
后端·rust·github
Kapaseker1 天前
Android程序员初学Rust-通道
后端·rust
我是前端小学生1 天前
如何在macos上安装rust开发环境
rust
UestcXiye1 天前
Rust 学习笔记:关于共享状态并发的练习题
rust
gregmankiw2 天前
C#调用Rust动态链接库DLL的案例
开发语言·rust·c#