Skip to content

并发基础与线程安全

1. 这是什么

Rust 的并发基础,核心是在回答两个问题:

  • 多个执行单元怎样一起工作
  • 它们共享数据时,怎样避免数据竞争和未定义行为

在很多语言里,并发问题往往是“程序能跑,但偶尔会错”。
而 Rust 的设计目标之一,就是尽量把这类问题前移到编译期。

一句话理解:

  • 并发让程序能同时处理更多事情
  • 线程安全保证这些“同时进行”的事情不会把数据搞坏

2. 为什么重要

现代程序很少是完全串行的。你会不断遇到这些场景:

  • 同时处理多个请求
  • 后台任务和主逻辑并行执行
  • 多核 CPU 上分摊计算
  • I/O 等待期间不让程序空转

但并发带来的风险也非常真实:

  • 两个线程同时改同一份数据
  • 一个线程读到另一个线程一半写完的数据
  • 锁顺序错了,程序死锁
  • 状态同步不清,结果偶现错误

Rust 对并发的重要价值在于:

  • 把一部分线程安全问题提前到类型系统和编译器层面
  • 降低“看起来没问题,线上随机炸”的概率

3. 先建立直觉

先不要一上来想线程 API,而是先建立一个朴素图景:

  • 并发:多件事在时间上交错推进
  • 并行:多件事真的在多个核上同时执行
  • 线程安全:不管调度顺序如何,数据都不会被破坏

Rust 看待并发,不只是“怎么开线程”,更是“数据怎么过边界”:

  • 值是被移动过去
  • 还是被共享过去
  • 如果共享,是否还有修改
  • 如果修改,靠什么约束

所以 Rust 的并发模型和所有权是连在一起的。

4. 核心内容

4.1 并发不只是开线程

很多初学者一说并发就想到:

rust
std::thread::spawn(...)

但真正困难的往往不是“开线程”,而是:

  • 线程之间怎么传数据
  • 共享状态怎么控制
  • 生命周期和所有权怎么满足
  • 程序退出时谁先结束

所以并发学习的重点不应该只是 API,而应该是:数据和任务之间的关系

4.2 Rust 为什么强调数据竞争

数据竞争可以粗略理解成:

  • 多个线程访问同一份内存
  • 至少有一个在写
  • 又缺乏正确同步

这类 bug 很可怕,因为它通常:

  • 不稳定
  • 难复现
  • 修起来非常痛苦

Rust 的强项在于,它会通过所有权、借用以及 Send / Sync 等规则,尽量让很多不安全共享方式在编译期就过不去。

4.3 两种常见思路:消息传递 vs 共享状态

Rust 并发里,最常见的两条路是:

路线一:消息传递

直觉上像:

  • 每个线程处理自己的状态
  • 通过通道把结果发出去
  • 尽量少共享同一份可变数据

这条路的好处是边界清楚,往往更容易维护。

路线二:共享状态

直觉上像:

  • 多个线程都能访问同一份数据
  • 但要通过锁或其他同步手段控制访问

这条路更接近很多传统后端程序写法,但复杂度也更高。

Rust 里通常会优先鼓励你先思考:能不能先用消息传递,而不是直接共享可变状态

4.4 线程之间的数据移动

Rust 创建线程时,经常会把值 move 进去:

rust
let data = vec![1, 2, 3];
std::thread::spawn(move || {
    println!("{:?}", data);
});

这里的核心是:

  • 新线程拿到数据所有权
  • 原线程不再继续使用那份值

这很符合 Rust 的一贯思路:

  • 与其让两个线程都碰同一份可变值
  • 不如把值明确交给某一个线程

这其实是在通过所有权减少并发复杂度。

4.5 什么是 SendSync

初学 Rust 并发时,经常会碰到 SendSync 相关报错。
可以先这样理解:

  • Send:一个值是否可以安全地在线程之间转移所有权
  • Sync:一个值是否可以安全地被多个线程共享引用

不要急着背非常正式的定义,先记住它们是在描述:

  • 这个类型跨线程是否安全
  • 这个类型被共享引用时是否安全

这两个 trait 是 Rust 并发安全体系里非常关键的底层规则。

4.6 共享状态常见组合:Arc<Mutex<T>>

如果多个线程要共同访问并修改同一份数据,最常见的组合就是:

rust
Arc<Mutex<T>>

可以拆开理解:

  • Arc:多个线程共享所有权
  • Mutex:同一时刻只允许一个线程修改

它常见,是因为它直观、通用,而且很好地把两个需求分开了:

  • 共享
  • 同步

当然,它不是“并发万金油”。如果使用不当,也会带来锁竞争和死锁风险。

4.7 为什么锁不是免费午餐

很多人从别的语言转到 Rust,会下意识觉得:

  • 有锁就安全了
  • Mutex 就万事大吉了

其实锁只是把问题从“无控制共享”变成“受控制共享”。
它能解决数据竞争,但还可能引入新的问题:

  • 锁持有时间太长
  • 不同锁获取顺序不一致
  • 频繁加锁导致性能差
  • 把本可拆开的状态硬塞进一把大锁里

所以更成熟的并发思路通常是:

  • 先尽量减少共享
  • 必须共享时再上锁
  • 上锁后尽量缩小临界区

4.8 不可变共享往往比可变共享容易

并发里最危险的通常不是“大家都在读”,而是“有人在写”。
所以一个很实用的工程判断是:

  • 如果能把共享数据设计成不可变
  • 或者改成消息传递更新
  • 往往比多线程共享可变状态更稳

这一点非常 Rust,也非常工程化。

4.9 线程安全不等于逻辑正确

Rust 可以帮你避免很多内存层面的并发错误,
但它不能自动保证业务逻辑一定正确。

比如:

  • 你的加锁范围不对
  • 更新顺序不符合业务要求
  • 两个线程之间虽然没有数据竞争,但整体状态仍然不一致

所以要记住:

  • Rust 能强力约束底层安全
  • 但高层并发设计仍然需要你自己想清楚

5. 常见误区

5.1 误区一:并发就是多开线程

不完整。
并发真正难的是任务协调和数据边界,而不是“线程数量”。

5.2 误区二:只要编译通过,并发逻辑就一定没问题

不是。
Rust 主要帮你约束内存安全和线程安全的一部分问题,业务级并发正确性仍要自己设计。

5.3 误区三:Arc<Mutex<T>> 是万能解法

它很常见,但不意味着所有共享问题都该这样建模。
有时通道或任务隔离会更合适。

5.4 误区四:锁只是性能问题,不影响设计

错误。
锁的引入会改变模块边界、访问方式和状态组织,本质上是设计问题,不只是性能细节。

6. 一个更实用的判断思路

写 Rust 并发代码时,可以先这样判断:

  1. 这件事真的要并发吗
  2. 能不能把任务拆开,各自拥有自己的数据
  3. 线程之间是传消息更合适,还是共享状态更合适
  4. 如果必须共享,是否能只共享不可变数据
  5. 如果必须共享可变数据,锁的粒度能不能做小
  6. 当前类型跨线程是否满足 Send / Sync 语义

按这个顺序想,通常比一上来就堆 Arc<Mutex<_>> 更稳。

7. 学习建议

建议按下面顺序学习 Rust 并发:

  1. 先理解线程创建与 move 捕获
  2. 再理解消息传递的思路
  3. 再学习 Arc<T>Mutex<T>RwLock<T> 的组合
  4. 再理解 SendSync 报错在表达什么
  5. 最后进入线程池、异步运行时和更高层的并发框架

这样可以先把“线程安全”的底层模型建立起来,再进入 Tokio 等更复杂的抽象层。

8. 自测标准

  • 能区分并发和并行的大致含义
  • 能解释 Rust 为什么把并发和所有权强绑定
  • 能说清消息传递和共享状态的基本差异
  • 能大致理解 SendSync 在表达什么
  • 能看懂 Arc<Mutex<T>> 这种组合的大致意图
  • 能意识到“线程安全”不等于“业务逻辑天然正确”