并发基础与线程安全
1. 这是什么
Rust 的并发基础,核心是在回答两个问题:
- 多个执行单元怎样一起工作
- 它们共享数据时,怎样避免数据竞争和未定义行为
在很多语言里,并发问题往往是“程序能跑,但偶尔会错”。
而 Rust 的设计目标之一,就是尽量把这类问题前移到编译期。
一句话理解:
- 并发让程序能同时处理更多事情
- 线程安全保证这些“同时进行”的事情不会把数据搞坏
2. 为什么重要
现代程序很少是完全串行的。你会不断遇到这些场景:
- 同时处理多个请求
- 后台任务和主逻辑并行执行
- 多核 CPU 上分摊计算
- I/O 等待期间不让程序空转
但并发带来的风险也非常真实:
- 两个线程同时改同一份数据
- 一个线程读到另一个线程一半写完的数据
- 锁顺序错了,程序死锁
- 状态同步不清,结果偶现错误
Rust 对并发的重要价值在于:
- 把一部分线程安全问题提前到类型系统和编译器层面
- 降低“看起来没问题,线上随机炸”的概率
3. 先建立直觉
先不要一上来想线程 API,而是先建立一个朴素图景:
- 并发:多件事在时间上交错推进
- 并行:多件事真的在多个核上同时执行
- 线程安全:不管调度顺序如何,数据都不会被破坏
Rust 看待并发,不只是“怎么开线程”,更是“数据怎么过边界”:
- 值是被移动过去
- 还是被共享过去
- 如果共享,是否还有修改
- 如果修改,靠什么约束
所以 Rust 的并发模型和所有权是连在一起的。
4. 核心内容
4.1 并发不只是开线程
很多初学者一说并发就想到:
std::thread::spawn(...)但真正困难的往往不是“开线程”,而是:
- 线程之间怎么传数据
- 共享状态怎么控制
- 生命周期和所有权怎么满足
- 程序退出时谁先结束
所以并发学习的重点不应该只是 API,而应该是:数据和任务之间的关系。
4.2 Rust 为什么强调数据竞争
数据竞争可以粗略理解成:
- 多个线程访问同一份内存
- 至少有一个在写
- 又缺乏正确同步
这类 bug 很可怕,因为它通常:
- 不稳定
- 难复现
- 修起来非常痛苦
Rust 的强项在于,它会通过所有权、借用以及 Send / Sync 等规则,尽量让很多不安全共享方式在编译期就过不去。
4.3 两种常见思路:消息传递 vs 共享状态
Rust 并发里,最常见的两条路是:
路线一:消息传递
直觉上像:
- 每个线程处理自己的状态
- 通过通道把结果发出去
- 尽量少共享同一份可变数据
这条路的好处是边界清楚,往往更容易维护。
路线二:共享状态
直觉上像:
- 多个线程都能访问同一份数据
- 但要通过锁或其他同步手段控制访问
这条路更接近很多传统后端程序写法,但复杂度也更高。
Rust 里通常会优先鼓励你先思考:能不能先用消息传递,而不是直接共享可变状态。
4.4 线程之间的数据移动
Rust 创建线程时,经常会把值 move 进去:
let data = vec![1, 2, 3];
std::thread::spawn(move || {
println!("{:?}", data);
});这里的核心是:
- 新线程拿到数据所有权
- 原线程不再继续使用那份值
这很符合 Rust 的一贯思路:
- 与其让两个线程都碰同一份可变值
- 不如把值明确交给某一个线程
这其实是在通过所有权减少并发复杂度。
4.5 什么是 Send 和 Sync
初学 Rust 并发时,经常会碰到 Send、Sync 相关报错。
可以先这样理解:
Send:一个值是否可以安全地在线程之间转移所有权Sync:一个值是否可以安全地被多个线程共享引用
不要急着背非常正式的定义,先记住它们是在描述:
- 这个类型跨线程是否安全
- 这个类型被共享引用时是否安全
这两个 trait 是 Rust 并发安全体系里非常关键的底层规则。
4.6 共享状态常见组合:Arc<Mutex<T>>
如果多个线程要共同访问并修改同一份数据,最常见的组合就是:
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 并发代码时,可以先这样判断:
- 这件事真的要并发吗
- 能不能把任务拆开,各自拥有自己的数据
- 线程之间是传消息更合适,还是共享状态更合适
- 如果必须共享,是否能只共享不可变数据
- 如果必须共享可变数据,锁的粒度能不能做小
- 当前类型跨线程是否满足
Send/Sync语义
按这个顺序想,通常比一上来就堆 Arc<Mutex<_>> 更稳。
7. 学习建议
建议按下面顺序学习 Rust 并发:
- 先理解线程创建与
move捕获 - 再理解消息传递的思路
- 再学习
Arc<T>、Mutex<T>、RwLock<T>的组合 - 再理解
Send、Sync报错在表达什么 - 最后进入线程池、异步运行时和更高层的并发框架
这样可以先把“线程安全”的底层模型建立起来,再进入 Tokio 等更复杂的抽象层。
8. 自测标准
- 能区分并发和并行的大致含义
- 能解释 Rust 为什么把并发和所有权强绑定
- 能说清消息传递和共享状态的基本差异
- 能大致理解
Send与Sync在表达什么 - 能看懂
Arc<Mutex<T>>这种组合的大致意图 - 能意识到“线程安全”不等于“业务逻辑天然正确”