线程与消息传递
本章把前面学到的所有权、trait 与智能指针组合起来,回答一个问题:Rust 凭什么敢说"无畏并发"。 主线是三种并发模型:线程、消息传递(channel)与共享状态(
Mutex/原子类型)。 前置知识:所有权与借用、 trait 与Send、 智能指针。
本章目标
- 能说清并发(concurrency)与并行(parallelism)的区别,以及
std::thread为什么选择 1:1 线程模型。 - 能准确复述"无畏并发"承诺的边界:排除了数据竞争(data race),但没有排除死锁、竞态条件(race condition)与内存泄漏。
- 能用
thread::spawn+move闭包、thread::scope、JoinHandle::join完成线程的创建、借用与结果回收。 - 能用
mpsc::channel/sync_channel实现多生产者单消费者,并在通道关闭时正确处理Err。 - 能写出
Arc<Mutex<T>>、Arc<RwLock<T>>、AtomicUsize三种共享状态方案,并说出各自的适用边界。 - 能解释
Send/Sync的精确定义与 auto trait 机制,看懂E0277 ... cannot be sent between threads safely。 - 能用
Condvar、Barrier、OnceLock、LazyLock、thread_local!解决具体的协作问题。 - 能区分数据竞争与竞态条件,并知道什么场景应该转向〈异步编程〉一章的异步。
概念地图:并发、并行与"无畏并发"的边界
并发不等于并行
并发(concurrency) 是"多个任务在重叠的时间段内推进",并行(parallelism) 是"多个任务在同一时刻真正同时执行"。并发是结构问题(如何组织任务), 并行是执行问题(需要多少硬件)。单核 CPU 上可以写出并发程序,但写不出并行程序。
| 维度 | 并发 | 并行 |
|---|---|---|
| 关注点 | 任务调度与协作 | 同时执行的吞吐 |
| 硬件要求 | 一个核就够 | 多核 / 多 CPU |
| 典型手段 | 线程切换、async、事件循环 | 多线程、SIMD、多进程 |
| 失败模式 | 死锁、饥饿、竞态条件 | 缓存伪共享、扩展性瓶颈 |
OS 线程 vs 绿色线程:std 为什么是 1:1
OS 线程(operating system thread,内核线程) 由操作系统调度,每个线程有独立的内核栈;绿色线程(green thread) 由用户态运行时调度, M:N 地把大量轻量任务复用到少量内核线程上。
| 模型 | 映射关系 | 调度者 | 切换成本 | 代表 |
|---|---|---|---|---|
| 1:1 | 1 个用户线程 ↔ 1 个内核线程 | 操作系统 | 高(陷入内核) | Rust std::thread、Java Thread、C++ std::thread |
| N:1 | N 个用户线程 → 1 个内核线程 | 用户态运行时 | 低 | 早期 Java 绿色线程 |
| M:N | M 个用户线程 ↔ N 个内核线程 | 用户态运行时 | 低 | Go goroutine、Erlang process |
| 异步 | 任务在少数线程上协作式让出 | 用户态执行器 | 最低 | Rust async + tokio |
🧠 原理:Rust 1.0 之前(0.1x 时代)内置过绿色线程,后来被移除。原因是:绿色线程必须绑定一个运行时(runtime),而运行时要做调度、要处理阻塞式系统调用、 要给每个任务分配栈——这些策略选择不该由语言强制。
std选择 1:1,让标准库不携带运行时;需要 M:N 时由tokio、async-std这类库在〈异步编程〉一章的async模型里提供。
代价也要说清楚:std::thread 的一个线程默认栈大小是 2 MiB(Windows 上由链接器/PE 决定,可用 Builder::stack_size 覆盖), 创建 10 万个线程会直接耗尽地址空间。所以"任务数量巨大且大多在等 I/O"时,正确的工具是异步,而不是线程。
💡 对照:Java 的
Thread也是 1:1(映射到内核线程),所以 Java 才有ExecutorService线程池来摊薄创建成本; Python 的threading.Thread同样是 1:1,但受 GIL 限制无法真正并行执行字节码;Go 的go f()是 M:N,创建成本极低。
"无畏并发"到底保证了什么
Rust 的官方口号是无畏并发(fearless concurrency)。它的准确含义是:
编译器在编译期静态地排除数据竞争(data race)。
数据竞争的定义需要同时满足三个条件:两个及以上的线程访问同一块内存;至少一个是写操作;没有任何同步手段。三者同时成立才叫数据竞争。 Rust 通过 Send / Sync 两个 auto trait(第 「Send 与 Sync 深入」一节)把"能跨线程传递/共享"变成类型系统里的约束,凡是不满足的一律编译失败。
但以下是不保证的,必须记牢边界:
| 问题 | Rust 编译期是否排除 | 说明 |
|---|---|---|
| 数据竞争(data race) | ✅ 排除 | Send/Sync + 所有权,安全 Rust 中不可能出现 |
| 竞态条件(race condition) | ❌ 不排除 | 逻辑上的"先检查后使用"依然可能出错,见「数据竞争 vs 竞态条件」 |
| 死锁(deadlock) | ❌ 不排除 | 两个 Mutex 反向加锁,编译完全合法 |
| 活锁 / 饥饿(livelock / starvation) | ❌ 不排除 | 自旋重试永不成功,或写锁永远抢不到 |
| 内存泄漏(memory leak) | ❌ 不排除 | Rc 循环引用、Box::leak、忘记 join 的线程 |
| 原子性缺失导致的不变式破坏 | ❌ 不排除 | Mutex 只保护你真正放进锁里的数据,见「MutexGuard 的能力与"保护范围"边界」 |
unsafe 代码里的 UB | ❌ 不排除 | 错误地 unsafe impl Send,见「unsafe impl Send/Sync:正确性与风险」 |
⚠️ 陷阱:初学者常把"无畏并发"理解成"并发一定正确"。正确的理解是:类型系统帮你消灭了一整类最难调试的 bug(数据竞争),剩下的逻辑正确性仍然要靠你自己设计。
std::thread:创建、借用与回收
spawn 与 join:最小的线程程序
下面这段代码演示最基本的模式:spawn 启动线程并返回 JoinHandle,join 阻塞当前线程直到目标线程结束并取回返回值。
rust
use std::thread;
use std::time::Duration;
fn main() {
// spawn 接收一个 FnOnce() -> T,返回 JoinHandle<T>
let handle: thread::JoinHandle<u32> = thread::spawn(|| {
for i in 1..=3 {
println!("子线程: {i}");
thread::sleep(Duration::from_millis(10));
}
6 * 7 // 闭包的最后表达式就是这个线程的返回值
});
println!("主线程继续做别的事");
// join 返回 Result<T, Box<dyn Any + Send>>,正常结束是 Ok
let answer = handle.join().unwrap();
println!("子线程返回: {answer}");
}text
输出(顺序可能交错,但子线程的三行一定在 join 之前打印完):
主线程继续做别的事
子线程: 1
子线程: 2
子线程: 3
子线程返回: 42🧠 原理:
spawn的签名是pub fn spawn<F, T>(f: F) -> JoinHandle<T> where F: FnOnce() -> T + Send + 'static, T: Send + 'static。 注意三个约束:FnOnce(线程体只执行一次)、Send(闭包要跨线程搬过去)、'static(闭包不能含短命借用)。这三个约束就是「为什么必须move」与 「Send与Sync深入」一节的全部动机。
为什么必须 move:FnOnce + Send + 'static
thread::spawn 无法知道新线程会活多久,所以它要求闭包满足 'static。如果闭包按默认规则借用外部变量,借用生命周期就受限于当前函数栈帧, 必然短于 'static,编译失败:
rust
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let h = thread::spawn(|| { // 编译错误 E0373
println!("{:?}", v); // v 是借用进来的
});
h.join().unwrap();
}text
error[E0373]: closure may outlive the current function, but it borrows `v`,
which is owned by the current function
--> src/main.rs:4:27
|
4 | let h = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
5 | println!("{:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
help: to force the closure to take ownership of `v` (and any other referenced variables),
use the `move` keyword
|
4 | let h = thread::spawn(move || {
| ++++加上 move 后,v 的所有权被搬进闭包,闭包类型因此满足 'static:
rust
use std::thread;
fn main() {
let v = vec![1, 2, 3];
// move 把 v 的所有权交给新线程;主线程从此不能再使用 v
let h = thread::spawn(move || v.iter().sum::<i32>());
println!("sum = {}", h.join().unwrap());
// println!("{:?}", v); // 取消注释会报 E0382:v 已被移走
}text
输出:sum = 6⚠️ 陷阱:
move是"按值捕获所有被用到的变量",而不是"只对需要移动的变量生效"。循环里连续spawn时,如果每个闭包都move同一个Arc, 第一次之后原变量就没了——这正是 「Arc<Mutex<T>>:完整的并发计数器」一节Arc::clone惯例存在的原因。
thread::scope:借用非 'static 数据(1.63+)
move 的代价是所有权被搬走,主线程再也用不了那些数据。作用域线程(scoped thread) std::thread::scope(最低稳定版本 1.63) 解决了这个矛盾:它保证作用域结束前所有子线程都已结束,因此编译器允许子线程借用作用域内的局部变量。
rust
use std::thread;
fn main() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
let (left, right) = data.split_at(4); // 两个 &[i32],生命周期短于 'static
// scope 会阻塞到闭包内所有 spawn 的线程结束
let total = thread::scope(|s| {
let a = s.spawn(|| left.iter().sum::<i32>()); // 借用 left,无需 move
let b = s.spawn(|| right.iter().sum::<i32>()); // 借用 right
a.join().unwrap() + b.join().unwrap()
});
println!("total = {total}");
println!("data 仍然可用: {data:?}"); // 所有权从未离开主线程
}text
输出:
total = 36
data 仍然可用: [1, 2, 3, 4, 5, 6, 7, 8]scope 的返回值就是闭包的返回值;如果作用域内任何线程 panic,scope 本身会在退出时 panic,消息为 a scoped thread panicked。 它内部帮你做了所有 join,所以闭包里不需要手动 join(当然手动 join 以便取回结果是可以的)。
⚠️ 陷阱:
scope只放宽了'static,没有放宽Send,也没有放宽借用检查。 下面这种想用两个线程分别修改同一个String的写法仍然报 E0499(同一时刻只能有一个可变借用):
rust
use std::thread;
fn main() {
let mut n = 0;
thread::scope(|s| {
s.spawn(|| { n += 1; }); // error[E0499]: cannot borrow `n` as mutable more than once
s.spawn(|| { n += 1; });
});
}正确做法是换成 AtomicUsize(「原子类型与内存序」一节)或 Mutex(「共享状态:Mutex、RwLock 与 Arc」一节)把可变性收进同步原语内部。
线程标识、可用并行度与 sleep
rust
use std::thread;
use std::time::Duration;
fn main() {
// ThreadId 是 Copy + Eq + Hash,可比较但不保证从 0 连续
println!("主线程 id = {:?}", thread::current().id());
let ids = thread::scope(|s| {
let handles: Vec<_> = (0..3)
.map(|_| s.spawn(|| thread::current().id()))
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect::<Vec<_>>()
});
println!("子线程 id = {ids:?}");
// 返回 Ok(非零并行度);在受限环境(如某些容器)里可能返回 Err
match thread::available_parallelism() {
Ok(n) => println!("可用并行度 = {n}"),
Err(e) => println!("无法获取并行度: {e}"),
}
// sleep 至少睡这么久,可能更久(受调度影响),不是精确计时器
thread::sleep(Duration::from_millis(50));
}text
一台 32 核机器上的实际输出(每次运行可能不同):
主线程 id = ThreadId(1)
子线程 id = [ThreadId(2), ThreadId(3), ThreadId(4)]
可用并行度 = 32💡 对照:
ThreadId类似 Java 的Thread.getId()、Pythonthreading.get_ident();available_parallelism()类似 Java 的Runtime.getRuntime().availableProcessors()与 Python 3.13 的os.process_cpu_count()。 注意ThreadId只保证在当前进程内唯一,且可被复用,不要拿它当业务主键。
panic 传播:join() 的 Result
子线程 panic 不会杀死整个进程(这与 C++ 的 std::terminate 不同),但 join() 会返回 Err,里面装的是 panic 的负载(payload)。
rust
use std::thread;
fn main() {
let ok = thread::spawn(|| 1 + 1);
println!("正常: {:?}", ok.join());
let bad = thread::spawn(|| panic!("子线程炸了: {}", 42));
let err = bad.join().unwrap_err(); // 这就是"传播":父线程显式决定如何处理
// panic! 的负载是 Box<dyn Any + Send>,需要 downcast 才知道具体类型
if let Some(msg) = err.downcast_ref::<&str>() {
println!("捕获到 &str 负载: {msg}");
} else if let Some(msg) = err.downcast_ref::<String>() {
println!("捕获到 String 负载: {msg}");
} else {
println!("捕获到未知负载(例如 panic_any 传入的自定义类型)");
}
}text
输出(顺序略有交错):
正常: Ok(2)
thread '<unnamed>' panicked at src/main.rs:7:31:
子线程炸了: 42
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
捕获到 &str 负载: 子线程炸了: 42🧠 原理:
panic!在线程边界被捕获并转成Result,这是 Rust 并发模型的重要设计:一个线程的逻辑失败不会破坏其他线程的内存状态。 这一点与共享内存模型配合得非常好——所以 「共享状态:Mutex、RwLock与Arc」一节反复强调"临界区尽量小、不要在持锁时 panic"。
⚠️ 陷阱:
panic!("...")产生&'static str负载,panic!("{}", x)产生String负载,而且不同版本可能不同, 所以稳健的代码两个都downcast_ref一遍(像上面那样),或者统一用std::panic::panic_any(MyError { .. })传自定义类型。
Builder:命名线程与调整栈大小
thread::Builder 用于在 spawn 之前配置属性,最常用的是 name(日志/调试器里可见)和 stack_size(深递归算法需要)。
rust
use std::thread;
fn main() {
// name 会出现在 panic 消息与调试器中:'worker-1' panicked at ...
let h = thread::Builder::new()
.name("worker-1".to_string())
.stack_size(4 * 1024 * 1024) // 4 MiB,默认通常为 2 MiB
.spawn(|| {
println!("我的名字是 {:?}", thread::current().name());
42
})
.expect("线程创建失败"); // Builder::spawn 返回 io::Result<JoinHandle<T>>
println!("结果 {}", h.join().unwrap());
}text
输出:
我的名字是 Some("worker-1")
结果 42注意 Builder::spawn 返回的是 io::Result<JoinHandle<T>>——系统资源有限, 创建线程可能失败(Windows 上是 ERROR_NOT_ENOUGH_MEMORY 之类)。生产代码里不要无条件 unwrap,至少用 expect 并说明原因, 或者向上传播 ?。
消息传递:mpsc 通道
Rust 官方文档有一句名言,来自 Go 社区:
Do not communicate by sharing memory; instead, share memory by communicating. (不要通过共享内存来通信,而要通过通信来共享内存。)
std::sync::mpsc 是 multi-producer single-consumer(多生产者单消费者)通道: Sender<T> 可以克隆出多个,Receiver<T> 只有一个。
基本 API
rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<String>(); // 类型可推断时也可写成 mpsc::channel()
thread::spawn(move || {
for i in 1..=3 {
// send 返回 Result<(), SendError<T>>:接收端全部消失时失败
tx.send(format!("消息 {i}")).unwrap();
}
// 这里 tx 被 drop,通道的最后一个生产者消失
});
// recv 阻塞直到拿到一条消息或所有发送端关闭
// 关闭时返回 Err(RecvError),循环因此自然终止
while let Ok(msg) = rx.recv() {
println!("收到: {msg}");
}
println!("通道已关闭,循环退出");
}text
输出:
收到: 消息 1
收到: 消息 2
收到: 消息 3
通道已关闭,循环退出关键语义速记:
| 调用 | 返回值 | 阻塞 | 语义 |
|---|---|---|---|
tx.send(v) | Result<(), SendError<T>> | 无界通道不阻塞 | 失败意味着所有 Receiver 都没了,SendError 里能取回 v |
rx.recv() | Result<T, RecvError> | 阻塞 | Err 表示所有 Sender 已 drop 且队列已空 |
rx.try_recv() | Result<T, TryRecvError> | 不阻塞 | Empty(暂时没数据)与 Disconnected(发送端全没了)要分开处理 |
rx.recv_timeout(d) | Result<T, RecvTimeoutError> | 最多阻塞 d | 超时是 Timeout,与 Disconnected 区分 |
rx.iter() | Iterator<Item = T> | 阻塞 | 等价于反复 recv().unwrap(),通道关闭会 panic |
rx.into_iter() | Iterator<Item = T> | 阻塞 | 与 iter() 相同,for msg in rx 用的就是这个 |
⚠️ 陷阱:
rx.iter()内部对recv()的结果做了unwrap,所以通道关闭时它会 panic,而不是优雅结束。 要优雅结束请手写while let Ok(m) = rx.recv()。只有在"通道不该关闭"的确定性场景下才用iter()。
多生产者:Sender::clone
Sender 实现了 Clone(Receiver 没有),所以可以复制任意多份分发给工作线程:
rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<u64>();
let mut handles = Vec::new();
for worker in 0..4u64 {
let tx = tx.clone(); // 每个线程一份 Sender
handles.push(thread::spawn(move || {
tx.send(worker * 10).unwrap();
}));
}
// 关键:主线程也要放弃自己那份,否则 rx.recv() 永远等不到"全部关闭"
drop(tx);
let mut sum = 0u64;
while let Ok(v) = rx.recv() {
println!("收到 {v}");
sum += v;
}
for h in handles {
h.join().unwrap();
}
println!("sum = {sum}");
}text
输出(顺序不确定):
收到 0
收到 10
收到 20
收到 30
sum = 60⚠️ 陷阱:忘记
drop(tx)是mpsc最常见的死锁原因。只要还有一个Sender活着,recv()就会一直阻塞等待"可能的未来消息"。
有界队列与背压:sync_channel
无界通道(channel)在生产快于消费时会无限增长,最终吃光内存。mpsc::sync_channel(n) 创建一个容量为 n 的有界通道, send 在队列满时阻塞,从而向上游施加背压(backpressure)。
rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::sync_channel::<u32>(2); // 缓冲区上限 2
let producer = thread::spawn(move || {
for i in 0..6 {
// 队列满时这里会阻塞,生产者被动降速
tx.send(i).unwrap();
println!("已发送 {i}(队列可能已满)");
}
});
thread::sleep(Duration::from_millis(30)); // 故意让生产者先跑满缓冲区
for _ in 0..6 {
println!(" 消费 {:?}", rx.recv());
thread::sleep(Duration::from_millis(5)); // 消费者慢,反压回去
}
producer.join().unwrap();
}text
输出特征:生产者会打印到 "已发送 1" 后停住,等消费者取走数据才继续
(具体交错依赖调度,但永远不会有超过 2 条消息滞留在通道里)。sync_channel(0) 是会合通道(rendezvous channel):容量为 0,send 必须等到 recv 配对成功才返回,常用于"交接棒"式的严格同步。
💡 对照:
sync_channel(n)相当于 Java 的ArrayBlockingQueue(n),sync_channel(0)相当于SynchronousQueue;channel()相当于LinkedBlockingQueue(无界)。 Go 的make(chan T, n)也是这个含义。
Receiver 不是 Sync:多消费者怎么办
Receiver<T> 是 Send 但不是 Sync。原因很直接:recv(&self) 需要串行化"从队列取走一条消息", 如果多个线程能同时持有 &Receiver 并调用 recv,就必须在标准库内部实现一套锁与唤醒逻辑——std 选择不付这个成本,而是把这个选择交给使用者。
因此下面这段代码编译失败:
rust
use std::sync::{mpsc, Arc};
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<i32>();
let rx = Arc::new(rx);
for _ in 0..3 {
let rx = Arc::clone(&rx);
thread::spawn(move || rx.recv().unwrap()); // 编译错误
}
tx.send(1).unwrap();
}text
error[E0277]: `std::sync::mpsc::Receiver<i32>` cannot be shared between threads safely
--> src/main.rs:9:31
|
9 | thread::spawn(move || rx.recv().unwrap());
| ------------- ^^^^^^^^^^^^^^^^^^ cannot be shared between threads safely
|
= help: the trait `Sync` is not implemented for `std::sync::mpsc::Receiver<i32>`
= note: required for `Arc<std::sync::mpsc::Receiver<i32>>` to implement `Send`
note: required because it's used within this closure
note: required by a bound in `spawn`
F: Send + 'static,
^^^^ required by this bound in `spawn`错误链条要会读: spawn 要求闭包 Send → 闭包捕获了 Arc<Receiver<i32>> → Arc<T>: Send 的前提是 T: Send + Sync → Receiver 满足 Send 但不满足 Sync → 整体不 Send。
三种正确模式:
模式 A(推荐):单消费者分发。 一个专门的"分发线程"持有 Receiver,把任务再通过其它机制(比如另一个 mpsc、Mutex 保护的队列、或原子索引)发给工作线程。
rust
use std::sync::mpsc;
use std::thread;
fn main() {
// 外部任务来源(本例未使用,只用来演示"主通道"的存在)
let (job_tx, job_rx) = mpsc::channel::<u32>();
// 每个工作线程一个私有通道:通道天然是单消费者的,所以完全不需要锁
let mut senders = Vec::new();
let mut handles = Vec::new();
for id in 0..3 {
let (tx, rx) = mpsc::channel::<u32>();
senders.push(tx);
handles.push(thread::spawn(move || {
let mut done = 0;
while let Ok(job) = rx.recv() {
println!("worker {id} 处理任务 {job}");
done += 1;
}
done // 通道关闭后返回处理条数
}));
}
// 把 Sender 与 JoinHandle 分开存放:前者交给分发者,后者留给主线程收尾
let dispatcher = thread::spawn(move || {
for job in 0..7u32 {
// 简单的轮询分发(真实项目里常用任务队列或 work-stealing 库)
senders[job as usize % senders.len()].send(job).unwrap();
}
senders.len() // 返回持有过的通道数,便于断言
});
// senders 已被移入 dispatcher;它返回时全部 drop → 工作线程的 recv 得到 Err
assert_eq!(dispatcher.join().unwrap(), 3);
let total: i32 = handles.into_iter().map(|h| h.join().unwrap()).sum();
assert_eq!(total, 7, "7 个任务必须全部被处理");
// 主通道从未被投递,接收端此刻应为 Empty 而不是 Disconnected
let empty = matches!(job_rx.try_recv(), Err(mpsc::TryRecvError::Empty));
println!("主通道为空: {empty}");
drop(job_tx);
println!("共处理 {total} 个任务");
}text
输出(worker 编号与顺序不定):
worker 0 处理任务 0
worker 1 处理任务 1
...
主通道为空: true
共处理 7 个任务模式 B:Mutex<Receiver>。 用锁把 recv 串行化,简单但所有消费者争同一把锁,扩展性差:
rust
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<u32>();
let rx = Arc::new(Mutex::new(rx));
let handles: Vec<_> = (0..3)
.map(|_| {
let rx = Arc::clone(&rx);
thread::spawn(move || {
// 锁只护住 recv 这一下,取到值立刻释放
let msg = rx.lock().unwrap().recv();
msg.ok()
})
})
.collect();
for i in 0..3 {
tx.send(i).unwrap();
}
drop(tx);
for h in handles {
println!("得到 {:?}", h.join().unwrap());
}
}模式 C:crossbeam-channel。 如果想直接获得多消费者(MPMC)语义,用成熟的三方库:cargo add crossbeam-channel, 它的 Receiver 是 Sync,可以直接 clone() 分发给多个线程,另外还提供 select!、无界/有界统一 API 与更高的吞吐。本章不展开, 知道"std 的 mpsc 不够用时有这条路"即可。
🚀 进阶:
std::sync::mpsc在 1.67 之后换成了基于crossbeam的更快实现, 但公开 API 与Receiver: !Sync的约束没有变。 性能敏感的多消费者场景请直接使用crossbeam-channel。
延伸阅读
- 同一概念的第二种讲法(官方书中文版、Rust 圣经的逐章映射),见 附录 E · 对照阅读与组合学习法。
- 官方文档、中文资料、书单与工具的完整索引,见 附录 D · 学习资源与文档索引。
async/await与 Tokio 是本章的直接后续,见异步编程;「守卫跨await」问题在那里展开。Rc/Arc/RefCell/Mutex的更多对照,见智能指针与闭包。