第 13 章:“无畏并发”不是梦——为什么 Rust 能让你自信地编写多线程代码

对于任何一位资深的 Java 开发者来说,并发编程都是一个既强大又充满凶险的领域。我们熟练地使用 ExecutorServicesynchronizedReentrantLockjava.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 中,我们会使用 synchronizedReentrantLock。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());
}

本章小结:“无畏”的秘密——SendSync Trait

这一切看似神奇,但背后是 Rust 严谨的类型系统在工作。编译器之所以知道哪些类型可以安全地在线程间传递和共享,是因为两个特殊的标记 trait

  • Send: 如果一个类型实现了 Send,意味着它的所有权可以安全地被转移到另一个线程。大部分类型(如 String, Vec<T>, Arc<T>)都是 Send 的。
  • Sync: 如果一个类型实现了 Sync,意味着它可以在多个线程间被安全地共享(即 &TSend 的)。大部分类型也是 Sync 的。Mutex<T> 就是 Sync 的。

你几乎不需要自己去实现这两个 trait。编译器会根据一个类型的组成部分自动推断它是否 SendSync。这套机制,就是 Rust 能够在编译期捕获数据竞争的底层逻辑。它不是魔法,而是严格的类型科学。

“无畏并发”是真的。 虽然死锁等逻辑问题依然可能存在,但最阴险、最难调试的数据竞争问题,已经被你的忠实伙伴——Rust 编译器——在代码诞生的那一刻就彻底消灭了。