对于任何一位资深的 Java 开发者来说,并发编程都是一个既强大又充满凶险的领域。我们熟练地使用 ExecutorService
、synchronized
、ReentrantLock
和 java.util.concurrent
包中的各种工具。但我们也深知其痛苦:数据竞争(Race Conditions)、死锁(Deadlocks)、ConcurrentModificationException
等问题,它们像幽灵一样在运行时出没,极难复现和调试。
Rust 在这里提出了一个石破天惊的口号:无畏并发(Fearless Concurrency)。
这并非说在 Rust 中编写并发代码是小事一桩,而是说 Rust 的编译器是你的安全网。它能在编译期就为你消除最大、最常见的一类并发 Bug——数据竞争。你之前费尽心力学习的所有权和借用规则,将在本章展现其真正的威力。它们不仅是内存安全的基石,更是无畏并发的源泉。
spawning 线程:所有权的跨线程之旅
在 Rust 中创建一个线程很简单,thread::spawn
就好,这很像 Java 的 new Thread(() -> { ... }).start()
。
但当我们想在线程中使用来自主线程的数据时,所有权规则立刻就体现出了它的价值。
use std::thread;
fn main() {
let v = vec![1, 2, 3];
// 下面的代码无法通过编译!
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
编译器会报错:“闭包可能比当前函数活得更久”。它在告诉你:我无法保证当你新创建的线程执行时,主线程中的 v
还存在(主线程可能先结束,导致 v
被销 y 毁)。
解决方案是使用 move
关键字,强制闭包获取它所使用值的所有权:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
// 使用 move 将 v 的所有权转移到新线程
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
// drop(v); // 如果在这里尝试使用 v,会编译失败,因为 v 已被移动
handle.join().unwrap();
}
通过 move
,我们确保了 v
会和新线程“同生共死”,其生命周期得到了保证。所有权系统像一位严谨的管家,确保了跨线程传递数据的安全性。
消息传递:通过通信来共享内存
Go 语言有一句名言:“不要通过共享内存来通信,而要通过通信来共享内存。” Rust 也将此奉为圭臬,并通过通道(Channels) 提供了强大的消息传递并发模型。
这非常类似 Java 中的 BlockingQueue
,但与 Rust 的所有权系统结合得天衣无缝。
Rust 的标准库提供了 mpsc
通道,代表“多个生产者,单个消费者”(multiple producer, single consumer)。
use std::sync::mpsc;
use std::thread;
fn main() {
// tx 是发送端 (transmitter),rx 是接收端 (receiver)
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
// send 方法会获取 val 的所有权
tx.send(val).unwrap();
// println!("val is {}", val); // 编译失败!val 的所有权已被移走
});
// recv() 会阻塞主线程,直到从通道接收到一个值
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
所有权内幕: 当 tx.send(val)
被调用时,val
的所有权被移动到了通道的另一端。发送方线程在发送之后,就无法再访问或修改 val
。这个机制从根本上杜绝了“发送后修改”这类常见的数据竞争 Bug。
共享状态并发:Mutex<T>
的安全之道
有时候,我们确实需要共享一块内存,比如一个共享的配置或缓存。在 Java 中,我们会使用 synchronized
或 ReentrantLock
。Rust 的对应物是 Mutex<T>
(互斥锁)。
但 Rust 的 Mutex
有一个绝妙的设计:它是一个包裹着数据的智能指针。你必须先获取锁,才能得到访问数据的“钥匙”。
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
// m.lock() 会请求锁,如果锁已被占用则阻塞
// .unwrap() 在这里用于处理锁可能“中毒”(poisoned)的情况
// lock() 返回一个叫做 MutexGuard 的智能指针
let mut num = m.lock().unwrap();
*num = 6;
} // 锁在这里被自动释放!
println!("m = {:?}", m);
}
安全内幕 (RAII 模式): m.lock()
返回的 num
是一个名为 MutexGuard
的智能指针。这个“卫兵”有一个非常重要的特性:当它离开作用域时(比如 }
处),它会自动调用 drop
方法,从而自动释放它所持有的锁。
这个被称为 RAII (Resource Acquisition Is Initialization) 的模式,是 Rust 安全性的又一大利器。它让你不可能忘记释放锁,从而彻底消除了 Java 中因忘记在 finally
块里调用 unlock()
而导致的死锁问题。
线程安全的共享:Arc<T>
如何让多个线程都拥有对同一个 Mutex
的访问权呢?我们不能把 Mutex
的所有权 move
给其中一个线程。
答案是使用 Arc<T>
,即**原子引用计数(Atomically Reference Counted)**类型。
Arc<T>
是 Rc<T>
的线程安全版本。你可以把它想象成 Java 世界里一个被多方持有的对象引用,但它是通过原子操作来管理引用计数的。
Arc::new(data)
创建一个Arc
实例。Arc::clone(&arc_instance)
不会深度拷贝数据,只会增加引用计数。- 当最后一个
Arc
指针离开作用域时,数据才会被清理。
Arc<Mutex<T>>
是并发 Rust 中用于共享可变状态的最常用、最地道的模式。
use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
// 使用 Arc 来包裹 Mutex,让它可以被安全地共享
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
// 为每个线程克隆一份 Arc 指针
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 在线程内锁定并修改数据
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
// 等待所有线程完成
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
本章小结:“无畏”的秘密——Send
和 Sync
Trait
这一切看似神奇,但背后是 Rust 严谨的类型系统在工作。编译器之所以知道哪些类型可以安全地在线程间传递和共享,是因为两个特殊的标记 trait
:
Send
: 如果一个类型实现了Send
,意味着它的所有权可以安全地被转移到另一个线程。大部分类型(如String
,Vec<T>
,Arc<T>
)都是Send
的。Sync
: 如果一个类型实现了Sync
,意味着它可以在多个线程间被安全地共享(即&T
是Send
的)。大部分类型也是Sync
的。Mutex<T>
就是Sync
的。
你几乎不需要自己去实现这两个 trait
。编译器会根据一个类型的组成部分自动推断它是否 Send
或 Sync
。这套机制,就是 Rust 能够在编译期捕获数据竞争的底层逻辑。它不是魔法,而是严格的类型科学。
“无畏并发”是真的。 虽然死锁等逻辑问题依然可能存在,但最阴险、最难调试的数据竞争问题,已经被你的忠实伙伴——Rust 编译器——在代码诞生的那一刻就彻底消灭了。