rust线程安全

0 阅读5分钟

在 Rust 中,“线程安全”不仅仅是一个概念,而是通过类型系统编译器检查在编译阶段强制保证的特性。Rust 的核心哲学是:如果代码能编译通过,那么它在运行时就是线程安全的(没有数据竞争)

Rust 通过两个核心 Trait 来定义和管理线程安全性:Send 和 Sync

1. 核心基石:Send 和 Sync

这两个 Trait 是 Rust 并发模型的自动推导基础,通常不需要手动实现(除非你在写 unsafe 代码)。

Send:所有权可以跨线程转移

  • 定义:如果一个类型 T 实现了 Send,那么它的所有权可以安全地从当前线程移动(Move) 到另一个线程。

  • 含义:该类型不包含任何“线程局部”资源或不可跨线程访问的指针。

  • 例子

    • i32StringVec<T> (当 T 是 Send 时) 都是 Send 的。
    • Rc<T> (引用计数指针) 不是 Send 的,因为它的引用计数操作不是原子的,跨线程修改会导致数据竞争。
    • *const T (原始指针) 默认不是 Send 的。

rust

编辑

use std::thread;

fn main() {
    let s = String::from("Hello");
    
    // String 实现了 Send,所以可以 move 进线程
    thread::spawn(move || {
        println!("{}", s);
    }).join().unwrap();
}

Sync:引用可以跨线程共享

  • 定义:如果一个类型 T 实现了 Sync,那么 &T (对 T 的不可变引用) 可以安全地在多个线程间共享。

  • 数学关系T 是 Sync 的 当且仅当 &T 是 Send 的。

  • 含义:允许多个线程同时读取该数据。注意:Sync 意味着可以同时可变访问(那是 Mutex 的工作),它只保证并发读取是安全的。

  • 例子

    • i32StringVec<T> 都是 Sync 的。
    • Cell<T>RefCell<T> 不是 Sync 的,因为它们允许在只有不可变引用的情况下进行内部可变性修改,这在多线程下会导致数据竞争。
    • Rc<T> 不是 Sync 的。

2. 如何实现线程安全的可变共享?

既然普通的变量不能直接在多线程中可变共享,Rust 提供了以下模式:

A. 消息传递 (Message Passing) - 首选

使用 std::sync::mpsc 通道。数据的所有权在线程间转移,同一时刻只有一个线程拥有数据,从根本上避免竞争。

  • 适用场景:管道处理、任务分发。

B. 共享内存 + 锁 (Shared Memory + Locks)

当必须共享状态时,使用智能指针包装互斥锁。

  • Arc<Mutex<T>> :

    • Arc (Atomic Reference Counting): 提供线程安全的引用计数,允许多个线程拥有同一个数据的“所有权句柄”。
    • Mutex: 保证同一时间只有一个线程能获取锁并修改内部数据 T
    • 原理Mutex<T> 实现了 Sync (前提是 T 是 Send),因为它通过锁机制序列化了对 T 的访问。

rust

编辑

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));
    let mut handles = vec![];

    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            // lock() 返回一个 MutexGuard
            // 只有拿到 guard 才能修改数据
            // 如果其他线程持有锁,这里会阻塞
            let mut num = data_clone.lock().unwrap();
            num.push(i); 
            // guard 离开作用域,锁自动释放
        });
        handles.push(handle);
    }

    for h in handles {
        h.join().unwrap();
    }
    println!("{:?}", *data.lock().unwrap());
}

C. 原子操作 (Atomics)

对于简单的计数器或标志位,使用 std::sync::atomic 模块下的类型(如 AtomicUsizeAtomicBool)。

  • 特点:无锁(Lock-free),性能比 Mutex 高,但功能有限(只能做简单的读改写操作)。
  • 内存序:需要理解 Ordering (Relaxed, Acquire, Release, SeqCst) 来保证指令重排的正确性。

rust

编辑

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    let val = Arc::new(AtomicUsize::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let val_clone = Arc::clone(&val);
        handles.push(thread::spawn(move || {
            // 原子增加,无需显式锁
            val_clone.fetch_add(1, Ordering::SeqCst);
        }));
    }

    for h in handles { h.join().unwrap(); }
    println!("结果: {}", val.load(Ordering::SeqCst));
}

3. 内部可变性 (Interior Mutability) 的线程安全版本

单线程中我们常用 Cell 和 RefCell 实现内部可变性,但它们不是线程安全的。
多线程中对应的替代品是:

  • Cell -> 无直接对应 (通常用 Atomcis 或 Mutex)
  • RefCell -> Mutex 或 RwLock
  • Rc -> Arc

表格

单线程类型多线程安全替代品说明
Rc<T>Arc<T>原子引用计数
RefCell<T>Mutex<T> / RwLock<T>运行时锁检查 vs 编译时借用检查
Cell<T>AtomicT仅限基本类型的原子操作

4. 常见陷阱与编译器保护

陷阱 1: 误用 Rc 在线程间

rust

编辑

// 错误示例
use std::rc::Rc;
use std::thread;

let rc = Rc::new(5);
thread::spawn(move || { // 编译错误!
    println!("{}", rc); 
});
// 报错信息大致为:Rc<i32> cannot be sent between threads safely
// 原因:Rc 没有实现 Send trait

陷阱 2: 死锁 (Deadlock)

Rust 的编译器无法检测死锁。如果你嵌套获取锁且顺序不一致,程序会挂起。

  • 解决:始终按固定顺序获取锁,或使用 try_lock()

陷阱 3: 锁中毒 (Poisoning)

如果一个线程在持有 Mutex 锁时 Panic 了,锁会被“毒化”(Poisoned)。其他尝试获取该锁的线程不会阻塞,而是立即收到一个 Err(PoisonError)

  • 目的:防止其他线程在数据处于不一致状态下继续操作。
  • 处理:可以使用 lock().unwrap_or_else(|e| e.into_inner()) 来恢复锁并尝试修复数据。

总结

Rust 的线程安全模型可以概括为:

  1. 默认隔离:线程间内存默认不共享,通过 move 转移所有权。
  2. 显式共享:若要共享,必须使用实现了 Sync 的类型。
  3. 可变共享:若要可变共享,必须加锁 (MutexRwLock) 或使用原子类型 (Atomic)。
  4. 编译期保证:只要你通过了编译器的检查(特别是 Send 和 Sync 约束),就绝对不会发生数据竞争(Data Race)。

这种设计虽然增加了初学者的认知负担,但它将原本需要在运行时测试甚至生产环境才能发现的并发 Bug,提前到了编写代码的阶段。