Rust 学习笔记 21:并发编程 (无畏并发)

“Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.” – Rob Pike

在很多语言中,并发编程是噩梦。数据竞争 (Data Race)、死锁 (Deadlock)、竞态条件 (Race Condition) 让人防不胜防。 Rust 号称 “Fearless Concurrency”(无畏并发)。它利用所有权和类型系统,在编译期就把数据竞争扼杀在摇篮里。

1. 使用线程

Rust 默认使用 1:1 线程模型,即一个 Rust 线程对应一个操作系统线程。

 1use std::thread;
 2use std::time::Duration;
 3
 4fn main() {
 5    thread::spawn(|| {
 6        for i in 1..10 {
 7            println!("hi number {} from the spawned thread!", i);
 8            thread::sleep(Duration::from_millis(1));
 9        }
10    });
11
12    for i in 1..5 {
13        println!("hi number {} from the main thread!", i);
14        thread::sleep(Duration::from_millis(1));
15    }
16}

注意:当主线程结束时,所有派生线程都会被强制终止,无论它们是否执行完。

2. Join Handles

为了让主线程等待子线程结束,我们需要保存 thread::spawn 的返回值(JoinHandle),并调用 join() 方法。

1let handle = thread::spawn(|| { ... });
2// ... 做点别的 ...
3handle.join().unwrap();

3. 线程与 move 闭包

如果在线程中使用了外部变量,必须小心所有权问题。

1let v = vec![1, 2, 3];
2
3let handle = thread::spawn(|| {
4    println!("Here's a vector: {:?}", v); // 报错!
5});

Rust 编译器会报错:由于线程可能活得比主线程长,它不能借用 v。必须使用 move 关键字强制把 v 的所有权移动到线程闭包中。

1let handle = thread::spawn(move || {
2    println!("Here's a vector: {:?}", v);
3});

一旦移入,主线程就不能再使用 v 了。这就是 Rust 保证内存安全的方式:同一时间只有一个所有者。

4. 小结

第21篇笔记。我们迈出了并发编程的第一步。 Go 语言有 Goroutine (M:N 模型),Rust 选择了原生线程 (1:1 模型) 作为标准库实现,但通过 async/await (在后续章节) 也支持类似 Goroutine 的轻量级并发。 但无论哪种模型,Rust 的所有权规则都适用。

下一篇,我们将探讨 共享状态并发 (Shared-State Concurrency)。既然 Rust 不让多线程同时拥有一个变量,那我们怎么共享数据呢?Mutex 和 Arc 将闪亮登场。


练习题

  1. 尝试在不使用 move 的情况下,在子线程引用主线程的变量,观察编译器的报错信息。
  2. 创建 10 个线程,每个线程通过 join 互相等待(类似于接力赛)。

思考题

为什么 Rust 标准库选择 1:1 线程模型而不是像 Go 那样的 M:N 模型?这对系统编程语言意味着什么?(提示:Runtime 大小、FFI 交互)。


本文代码示例

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读