编程语言研究书库

无畏并发:线程、通道与共享状态 — 第 16 章拆解

无畏并发:线程、通道与共享状态

这一章讲什么: 多线程编程,以及 Rust 凭什么让它"无畏"。核心一句话: 所有权和类型检查把并发错误从"运行时的灵异 bug"变成"编译期的类型错误"—— 这正是第 4 章借用心智模型在多线程下的兑现。两条并发路线都会走一遍: 消息传递(通道)和共享内存(Mutex);最后用 Send/Sync 解释这一切为什么成立。

1. 顶层全景:一条主线,三次编译错误

主线例子:10 个线程共用一个计数器,各自 +1,最后打印 10。它会连撞三次编译错误,每次修一层:

  错误 1:move 闭包把 counter 移进循环,第二次迭代它已不在 → 需要多主
  错误 2:换 Rc,编不过——"Rc<Mutex<i32>> cannot be sent between threads safely" → 需要线程安全的计数
  错误 3:换 Arc,编译通过,Result: 10 ✓

  图说:三次报错=三次补课:所有权不可复制、Send 边界、Arc 的存在理由。

2. 线程基础

thread::spawn(闭包) 建线程;主线程结束,所有子线程无论跑到哪都被强杀——所以 spawn 的返回值 JoinHandle 要存下来,.join() 阻塞等待该线程跑完;join 写在哪,决定两个线程是交替还是串行1。线程间传数据:move 闭包把值的所有权搬进新线程(第 13 章预告过)。这里有个绕不过的检查:闭包默认借捕获,但新线程可能比创建它的函数活得久——编译器要求传给 spawn 的闭包满足 'static(E0373);move 强制夺所有权,正是解法。而"move 之后再想 drop 原值"会吃 E0382:move 覆盖保守的借用推断,但不允许违反所有权2。

3. 消息传递:mpsc 通道

标准库方案 std::sync::mpsc(multiple producer, single consumer):多个发送端、一个接收端。mpsc::channel() 返回 (tx, rx) 元组3。

let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    tx.send(String::from("hi")).unwrap();   // 接收端被 drop 时 send 报错
});
let received = rx.recv().unwrap();          // 阻塞等待

要点:recv 阻塞、try_recv 不阻塞;rx 可以直接 for 迭代(信道关闭即迭代结束);多生产者用 tx.clone()。关键机制:send 会拿走值的所有权——发送后想再用 val 就是 E0382:别的线程可能正在改它或已经 drop 它,所有权系统替你拦下。书里点题:这就是 Go 口号"不要共享内存来通信,要通信来共享内存"在 Rust 的所有权实现——通道即所有权的转移,一次只能有一方持有4。

4. 共享状态:Mutex 与 Arc

Mutex(互斥锁):任一时刻只允许一个线程访问数据;用前必须拿锁,用完必须放锁。 麦克风比喻:忘了交麦,全场沉默。Rust 的做法:"必须拿锁"由类型系统担保——m 的类型是 Mutex<i32> 而不是 i32,想摸到里面的数只能走 .lock();"必须放锁"由 Drop 担保——lock 返回的 MutexGuard 出作用域自动放锁,想忘都忘不掉5。这就是"Rust 让你锁不错"的完整机制:错用法根本编不过。

Arc<T> = 线程安全的 Rc:引用计数的增减用原子操作(atomic),所以能跨线程。代价是性能税——只在真正跨线程时才付这笔税,所以 Rc 没被 Arc 取代6。

对照表(书里明说):Mutex 之于 Arc,正如 RefCell 之于 Rc——都是"内部可变性":一个 immutable 的 Arc<Mutex<T>> 里能拿到可变引用;借用规则在 Mutex 那里从编译期挪到了"运行时锁",违反(如死锁)表现为挂起而非编译错7。Rust 护不住的两类并发逻辑错:引用循环(泄漏)和死锁——死锁是"两个线程各拿一把锁互相等",书末留作练习。

5. Send 与 Sync:为什么这一切成立

本章几乎所有并发设施都在标准库里,不在语言里;真正烙进语言的是 Send 和 Sync 这两个标记 trait(marker trait,没有方法,只表达类型属性)8:

  • Send:类型的值可以安全地把所有权转移到别的线程。 几乎所有类型都实现;反例是 Rc<T>——两个线程同时 clone/drop 会让计数出错,所以 Rc 故意不实现 Send,单线程用、不付线程安全税。完全由 Send 类型组成的类型自动是 Send(裸指针除外)。
  • Sync:类型的不可变引用可以安全地从多个线程访问(等价于 &T: Send)。Rc、RefCell/Cell 族都不实现;Mutex 实现。

手动实现 Send/Sync 是 unsafe——第 20 章;用现成部件拼新并发类型,编译器替你守不变量。这解释了第 16-4 那个错误的措辞:不是"你不该这么写",而是"这个类型根本没承诺它能跨线程"——类型系统把承诺变成了签名的一部分。

6. 作者的判断与证据

  • "所有权+类型系统把 data race 挡在编译期"——核心主张,机制链条完整(Send/Sync → spawn 的 bound → E0277)8。
  • "通道像单主、共享内存像多主"——概念类比,帮助选路线9。
  • "Mutex 能防住用错锁,防不住死锁"——诚实的边界声明7。

判断(我们的,不是书里的): 本章最值得带走的是**"并发安全 = 所有权不变量 + 两个标记 trait"** 这个还原——它解释了为什么 Rust 的并发没有发明新概念,却让 data race 成为编译错误: 不变量(同一时刻最多一个写者)第 4 章就有了,Send/Sync 只是把"类型是否参与并发" 变成编译器可检查的签名。"无畏"不是并发变容易了,而是错误提前了。 如果错,会错在: "编译过就无 data race"的前提是 safe Rust 且类型作者没乱标 Send/Sync; unsafe 手写并发原语(或依赖标错的库)会击穿这个保证——书把手动实现标为 unsafe 正是此意。

7. 边界与局限

  • 原子类型(std::sync::atomic)只说"存在",细节留给文档。
  • 死锁只点了名, mitigation 是练习。
  • 1:1 线程模型之外的方案(M:N、绿色线程)指给 crates,async 留到第 17 章。

8. 可带走的

  1. spawn 建线程;主线程结束子线程全死;join 等它跑完,位置决定交替或串行。
  2. 传给 spawn 的闭包要 move(或 'static),因为子线程活得可能更久。
  3. mpsc:多生产者单消费者;tx.send 拿走所有权;rx 可当迭代器。
  4. Mutex:类型即锁,lock 拿锁,MutexGuard 出作用域自动放锁。
  5. 跨线程共享计数用 Arc(原子计数),Rc 不行(不实现 Send)。
  6. Arc<Mutex<T>> ≈ 多线程版的 Rc<RefCell<T>>。
  7. Send = 可跨线程转移;Sync = 可跨线程共享引用;全由 Safe 部件组成的类型自动实现。
  8. 编译器防 data race 和错锁;防不了死锁和循环引用——这两类要自己做对。

9. 原文地图

主题原书章原文位置
spawn 与 JoinHandleChapter 16: Fearless Concurrencytext/23-ch16-chapter-16-fearless-concurrency.txt:81(搜「spawn」) · text/23-ch16-chapter-16-fearless-concurrency.txt:139(搜「JoinHandle」)
move 闭包与 'static同上text/23-ch16-chapter-16-fearless-concurrency.txt:263(搜「may outlive」)
mpsc 通道同上text/23-ch16-chapter-16-fearless-concurrency.txt:415(搜「mpsc::channel」) · text/23-ch16-chapter-16-fearless-concurrency.txt:486(搜「recv」)
send 移走所有权同上text/23-ch16-chapter-16-fearless-concurrency.txt:540(搜「borrow of moved value」)
Mutex API 与守卫同上text/23-ch16-chapter-16-fearless-concurrency.txt:727(搜「Mutex::new」) · text/23-ch16-chapter-16-fearless-concurrency.txt:751(搜「MutexGuard」)
Rc 不可跨线程同上text/23-ch16-chapter-16-fearless-concurrency.txt:863(搜「cannot be sent」)
Arc同上text/23-ch16-chapter-16-fearless-concurrency.txt:902(搜「Atomic Reference Counting」)
Mutex 与死锁边界同上text/23-ch16-chapter-16-fearless-concurrency.txt:971(搜「deadlocks」)
Send / Sync同上text/23-ch16-chapter-16-fearless-concurrency.txt:988(搜「marker traits」)

Footnotes

  1. 出处:「Chapter 16: Fearless Concurrency」第 107 段(text/23-ch16-chapter-16-fearless-concurrency.txt:107,搜「shut down」)与第 139 段(text/23-ch16-chapter-16-fearless-concurrency.txt:139,搜「JoinHandle」)。 ↩

  2. 出处:「Chapter 16: Fearless Concurrency」第 263 段(text/23-ch16-chapter-16-fearless-concurrency.txt:263,搜「may outlive」)与第 375 段(text/23-ch16-chapter-16-fearless-concurrency.txt:375,搜「overrides Rust’s conservative」)。 ↩

  3. 出处:「Chapter 16: Fearless Concurrency」第 415 段(text/23-ch16-chapter-16-fearless-concurrency.txt:415,搜「mpsc::channel」)。 ↩

  4. 出处:「Chapter 16: Fearless Concurrency」第 551 段(text/23-ch16-chapter-16-fearless-concurrency.txt:551,搜「compile-time error」)与第 385 段(text/23-ch16-chapter-16-fearless-concurrency.txt:385,搜「share memory by」)。 ↩

  5. 出处:「Chapter 16: Fearless Concurrency」第 751 段(text/23-ch16-chapter-16-fearless-concurrency.txt:751,搜「MutexGuard」)。 ↩

  6. 出处:「Chapter 16: Fearless Concurrency」第 902 段(text/23-ch16-chapter-16-fearless-concurrency.txt:902,搜「Atomic Reference Counting」)。 ↩

  7. 出处:「Chapter 16: Fearless Concurrency」第 971 段(text/23-ch16-chapter-16-fearless-concurrency.txt:971,搜「deadlocks」)。 ↩ ↩2

  8. 出处:「Chapter 16: Fearless Concurrency」第 988 段(text/23-ch16-chapter-16-fearless-concurrency.txt:988,搜「marker traits」)。 ↩ ↩2

  9. 出处:「Chapter 16: Fearless Concurrency」第 12 段(text/23-ch16-chapter-16-fearless-concurrency.txt:12,搜「ownership」)。 ↩

On this page