Skip to content

并发编程

并发编程的四个核心问题是递进的:多个执行单元如何共享状态而不出错、各自看到的顺序与可见性如何保持一致、需要等待时如何协调与阻塞、任务失败时如何传播与隔离。对应的解决思路构成并发编程的知识版图:

核心问题解决思路对应文章
共享状态锁、原子操作、消息传递、不共享访问自肃同步原语原子操作
顺序与可见性内存屏障、happens-before、语言内存模型内存屏障原子操作
协调与阻塞条件变量、channel、async/await、超时取消线程同步原语
失败传播结构化并发、supervisor、失败隔离无锁并发

访问自肃

并发编程有一条反复被验证的思想主线:共享数据的每一次访问都必须经过一个已知的同步通道。锁、原子变量、消息传递、不可变数据——形式各异,但都在执行同一条纪律:不允许"裸访问"共享状态。这条纪律称为访问自肃(Access Discipline)——访问共享数据时的自我约束。

并发 bug 的共同结构

所有数据竞争都有同一个结构:两个线程访问同一块内存,至少一个是写,且两者之间没有同步关系。关键不在于"两个线程都访问了",而在于"访问之间没有同步通道"。

这指向一个反直觉的事实:并发 bug 从来不是"访问了不该访问的数据",而是"通过未登记的路径访问了数据"。线程 A 持锁访问 counter,线程 B 不持锁直接读——A 遵守纪律而 B 没有,但后果由双方承担。锁的效力依赖"所有访问者都使用同一把锁"的约定——这个约定就是访问自肃。

工程推论:同步机制的价值不在于它本身,而在于访问路径的覆盖率。一把保护 95% 访问路径的锁,安全性约等于没有锁。审查并发代码的第一个问题不是"有没有加锁",而是"有没有未加锁的访问路径"。

四种形态

锁的自肃:所有访问者共享同一把锁,访问前获取、访问后释放。脆弱点在锁的粒度(锁保护数据而非代码,但思维习惯按代码块组织锁)和锁顺序(多锁交叉依赖时必须遵守的锁序约定,违反即死锁)。

原子操作的自肃:被原子变量保护的数据,所有访问必须走原子 API。C/C++ 中一条普通读写路径就足以破坏整个原子方案;Rust 把这条纪律做进类型系统——原子类型没有非原子访问的语法,裸访问是编译错误。

消息传递的自肃:不共享内存,通过通道转移数据所有权。Go 的"通过通信共享内存"是这条纪律的口号。违反方式是"发了消息但保留引用"——发送方继续使用已发送的数据,通道语义被破坏。Rust 的所有权系统在编译期阻止这种违反。

不可变的自肃:共享数据标记为不可变——初始化后永不修改。这条纪律特殊之处在于不需要同步通道——不可变数据的读取天然线程安全。函数式语言的并发优势源于默认不可变;工程中的实用形态是"配置数据启动时冻结,运行期只读"。

与类型系统的关系

四种形态中,不可变和消息传递可以被类型系统强制执行,锁和原子操作依赖开发者纪律——除非语言提供更强的工具。

Rust 的 Send/Sync trait 是访问自肃的系统化表达:Sync 类型可安全共享引用,Send 类型可安全跨线程转移。编译期检查保证不满足 Sync 的类型(内部可变且未同步)无法被多线程共享——"裸访问共享可变状态"从坏习惯变为编译错误。纪律从文档约定提升为类型约束,覆盖率检查从代码审查转移到编译器。

需要明确 Rust 承诺的边界:它防止的是数据竞争(两条路径同时裸访问),而不是死锁、活锁等逻辑并发问题。访问自肃解决"访问路径是否登记",不解决"同步策略是否合理"——锁的粒度和顺序仍然是开发者责任。

工程实践

共享状态登记表。维护一份全局/共享变量清单,每项标注同步通道(锁名、原子类型、消息通道或不可变标记)。新代码访问共享状态先查表——未登记的路径就是潜在竞争。在 C/C++ 工程中这把隐式约定显式化,价值最大。

审查焦点是路径而非代码。列出"这块数据的所有读写点",逐一检查与同步通道的关系。一眼看上去加了锁的代码可能漏了读路径——"读不加锁"是"读便宜"心理的产物,但并发读与写竞争同样是数据竞争。

不可变化是成本最低的自肃。每引入新共享数据先问"必须可变吗"——配置、常量表、只读索引默认不可变,变动隔离到最小范围。每减少一个可变共享状态,就消灭一类潜在竞争。

访问自肃的最终形态是一个认知转变:并发安全不是"加够锁"的状态,而是"所有访问路径都在同步通道覆盖之下"的不变量。锁、原子、消息、不可变是四种实现——选择取决于数据访问模式,但不变量的存在本身不可谈判。

共享内存模型(最难)

C/C++、Java 底层、pthread 的路线:锁、mutex、atomic 直接操作共享内存。性能上限最高、心智负担最重。适合内核、高频交易等极致性能场景。这条路线的一切正确性都建立在访问自肃之上——所有访问路径必须登记在同步通道上,同步原语提供通道本身,原子操作提供无锁的基础原语,内存屏障保证跨线程的顺序关系。

消息传递模型(最稳)

Go channel、Erlang/Actor、Rust channel 的路线:不共享内存,通过消息传递状态,天然隔离。适合服务端、微服务、分布式系统。数据的单一所有权替代了锁的互相谦让——正确性由通道语义保证,而非开发者对每一条访问路径的审查。

async/await(时间抽象)

JS、Rust async、Java CompletableFuture 的路线:协作式并发,单线程也可高并发。关键误区:async 解决的是"等"(IO 等待期间让出执行权),不是"共享"(共享状态的安全仍需上述两条路线的机制)。async 不等于线程安全。