blog.dopana

Back

Rust 中级系列第 15 篇。并发编程(Concurrency)历来以高难度和易出错著称 —— 数据竞争(Data Race)、死锁以及内存破坏是多线程系统中最头疼的隐患。

Rust 提出了 Fearless Concurrency(无畏并发) 的理念:依靠所有权与类型系统,将并发安全隐患直接转化为编译期错误,从而在编译阶段杜绝绝大多数并发 Bug。

graph TD
    A["Rust 无畏并发"] --> B["原生线程创建<br/>std::thread::spawn"]
    A --> C["消息传递通信<br/>Channels (mpsc)"]
    A --> D["共享状态并发<br/>Arc<T> + Mutex<T>"]
    A --> E["可扩展并发安全<br/>Send & Sync 特征"]

浅显易懂(ELI5):繁忙的餐厅后厨#

想象一家餐厅的后厨工作:

  1. 单线程模式:只有 1 位大厨负责洗菜、切配、炒菜到洗碗的全部流程。绝对安全,但吞吐量有限。
  2. 缺乏管制的传统多线程(C/C++):5 位厨师在没有规则的情况下同时争抢同一把刀和案板,极易误伤彼此或切错食材(数据竞争与内存破坏)。
  3. Rust 无畏并发:每位厨师拥有专属工位(所有权),通过点单传送带传递菜品需求(通道 Channel)。当必须共用特制铜锅时,必须刷卡获取锁(Arc + Mutex)。主厨(编译器)在开门营业前会对所有协作规范进行严格审查!

1. 使用原生线程并发执行代码#

在 Rust 中,你可以使用 thread::spawn 创建操作系统原生线程:

使用 move 闭包转移所有权#

当子线程需要使用外部作用域的数据时,可以通过 move 关键字显式将变量的所有权转移给线程闭包:

src/main.rs
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("线程内的动态数组: {v:?}");
    });

    handle.join().unwrap();
}
rust

2. 消息传递:通道(Channels mpsc#

Go 社区的名言在 Rust 中同样适用:“不要通过共享内存来通信,而要通过通信来共享内存。”

Rust 标准库提供了 mpsc(multiple producer, single consumer:多生产者,单消费者)通道:

3. 共享状态并发:Arc<T>Mutex<T>#

当多个线程需要共同读写同一块内存时:

  • Mutex<T>(互斥锁):保证同一时刻只允许一个线程持有锁并访问其包装的数据。
  • Arc<T>(原子引用计数):一种线程安全的智能指针,允许多个线程共享同一个 Mutex 的所有权。

[!NOTE] Rc<T> 并不是线程安全的,因为它的计数器变更未采用原子操作。Rust 编译器会直接拒绝跨线程传递 Rc<T>,强制使用 Arc<T>

4. 并发基石:SendSync 特征#

Rust 的并发安全模型建立在两个核心 auto traits 之上:

  1. Send:表明该类型的所有权可以在线程间安全传递。
  2. Sync:表明该类型的不可变引用(&T)可以在多个线程间安全共享(T: Sync 等价于 &T: Send)。

Rust 绝大多数基础类型默认都实现了 SendSync。若试图跨线程共享不安全的类型(如 Rc<T>),编译器会在构建阶段直接阻止编译。

总结#

  • thread::spawn 配合 move 闭包实现安全的所有权转移与多线程启动。
  • mpsc::channel 基于所有权模型构建了可靠的多生产者单消费者通信。
  • Arc<Mutex<T>> 是跨线程安全共享与修改可变状态的标准组合拳。
  • SendSync 特征将数据竞争的预防深度融入了编译期类型检查。

参考资料#