Rust 的强类型系统和所有权机制为在多线程环境下安全地处理数据提供了基础。从本质上讲,并发编程需确保多线程间的数据访问既高效又避免数据竞争或未定义行为。
Rust 主要支持两类并发模型:一是基于消息传递的并发(通过信道 channel 进行线程间通信),二是基于共享内存的并发(利用同步原语如 Mutex、RwLock 管理对共享数据的访问)。 这两种方式分别强调了线程间隔离及安全共享的数据管理,有效提升了并发编程的可靠性和安全性。

spawn 与 join在 Rust 中,我们使用 thread::spawn 函数来创建新的线程。这个函数接受一个闭包作为参数,该闭包包含了新线程要执行的代码。当我们调用 thread::spawn 时,它会立即返回一个 JoinHandle,这个句柄代表了新创建的线程。
JoinHandle 是一个重要的类型,它允许我们与创建的线程进行交互。最常用的方法是 join(),它会阻塞当前线程,直到目标线程执行完毕。join() 方法返回一个 Result,如果线程正常结束,结果包含线程的返回值;如果线程发生 panic,则包含错误信息。
当我们需要在新线程中使用主线程的数据时,通常需要使用 move 关键字。move 闭包会获取所引用变量的所有权,将这些数据从主线程"移动"到新线程中。这种设计有两个重要作用:
move,闭包可能只是借用主线程的数据。但主线程可能在新线程使用这些数据之前就结束了,导致新线程访问已经被释放的内存。下面是一个简单的例子,展示了如何使用 thread::spawn 创建一个新线程,并使用 join 等待线程执行完毕。
|use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(move || { thread::sleep(Duration::from_millis(50)); 42 }); println!("result = {}", handle.join().unwrap()); }
|result = 42
Rust 标准库提供的多生产者单消费者(mpsc)通道是实现消息传递并发的核心工具。这种模式的基本思想是:不同的线程通过发送和接收消息来协调工作,而不是直接共享内存中的数据。
通道由两部分组成:发送端(Sender)和接收端(Receiver)。我们可以创建多个发送端的克隆,让不同的工作线程各自持有一个发送端,然后将处理结果发送到同一个接收端。主线程(或专门的汇总线程)通过接收端收集所有工作线程的结果。
这种设计的优势在于避免了共享可变状态带来的复杂同步问题。当数据通过通道传递时,发送方会失去对数据的所有权,接收方获得数据的完整控制权。这样就不存在多个线程同时访问同一块数据的情况,从根本上消除了数据竞态的风险。
使用 mpsc::channel() 函数可以创建一个通道,它返回一个元组:(Sender<T>, Receiver<T>)。发送端可以通过 clone() 方法复制多份,但接收端是唯一的。当所有发送端都被丢弃时,接收端的迭代器会自然结束,这为我们提供了一种优雅的终止机制。
下面是一个简单的例子,展示了如何使用 mpsc 通道实现消息传递并发。
|use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); for i in 0..3 { let txi = tx.clone(); thread::spawn(move || { txi.
|sum = 5
Arc<Mutex<T>>:共享可变状态当我们需要在多线程间共享并修改状态时,Arc<Mutex<T>> 是最经典的解决方案。这个组合巧妙地将两个重要概念结合在一起:
Arc(Atomically Reference Counted)是一个原子引用计数智能指针,它允许多个所有者同时拥有同一份数据。与 Rc<T> 不同,Arc<T> 使用原子操作来管理引用计数,因此可以安全地在多线程环境中使用。当我们需要将同一份数据传递给多个线程时,每个线程都可以拥有一个 Arc 的克隆,这样就实现了跨线程的共享所有权。
Mutex<T>(Mutual Exclusion)是一个互斥锁,它确保在任何时刻只有一个线程可以访问被保护的数据。当某个线程获得锁时,其他尝试获取同一个锁的线程会被阻塞,直到锁被释放。这种机制保证了对共享数据的访问是串行化的,从而避免了数据竞态。
将两者结合使用时,Arc 负责解决"如何在多线程间共享同一份数据"的问题,而 Mutex 负责解决"如何安全地修改共享数据"的问题。每当线程需要修改数据时,必须先调用 lock() 方法获取锁,然后才能访问内部的数据。这种设计确保了线程安全性,同时保持了相对简单的编程模型。
下面是一个简单的例子,展示了如何使用 Arc<Mutex<T>> 实现多线程共享可变状态。
|use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10
|10
RwLock<T>:读多写少的并行在许多实际应用中,我们经常遇到读多写少的场景:数据被频繁读取,但只是偶尔更新。典型的例子包括配置文件、缓存数据、统计信息等。在这类场景中,如果使用普通的 Mutex<T>,即使是纯读操作也需要获取独占锁,这会导致读操作之间产生不必要的竞争,严重影响并发性能。
RwLock<T>(读写锁)专门为这种场景设计。它允许多个线程同时获取读锁来并发读取数据,但写操作需要获取独占的写锁。这种设计大大提升了读密集型应用的并发性能,因为多个读操作可以真正并行执行,而不需要互相等待。
读写锁的工作机制遵循以下规则:
通过 read() 方法获取读锁,通过 write() 方法获取写锁。这种细粒度的锁控制让我们能够根据操作类型选择合适的锁级别,从而在保证数据安全的同时最大化并发性能。
下面是一个简单的例子,展示了如何使用 RwLock<T> 实现读多写少的并发。
|use std::sync::{Arc, RwLock}; use std::thread; fn main() { let data = Arc::new(RwLock::new(0)); let readers: Vec<_> = (0..3).map
并发的关键在于选择合适的模型:消息传递避免共享状态,适合“生产者-消费者”流程;共享可变状态在小范围内更直观,但要靠 Arc/Mutex/RwLock 明确同步边界。
Rust 的类型系统提供了编译期防护网,让我们在高效并行的同时保持可维护性。