首页 › programming › 你的多线程代码为什么会"见鬼"

你的多线程代码为什么会"见鬼":弱内存模型一次讲透

导语

先看一个让我后背发凉的视频片段:伦敦帝国理工学院的 Azalea Raad 教授在镜头前运行了一个只有两个线程、四条指令的小程序。教科书(准确说是 Leslie Lamport 的顺序一致性模型)说,某个输出结果不可能出现。她把程序跑在一个循环里,528 次迭代之后——那个"不可能"的结果出现了。第二次出现是在 5000 次左右。

这就是 Computerphile 这期《Why Multi-Threaded Code Can Sometimes Misbehave》讲的事情:你以为的并发执行顺序,和 CPU 真实的执行顺序,是两回事。这不是玄学,也不是 bug,而是现代硬件和编译器"合法"的优化行为,业内叫它"弱内存并发"(Weak Memory Concurrency)。

为什么值得你花 16 分钟看、再花 10 分钟读这篇解读?因为这类问题极其阴险:它平时不发作,压测不出现,一上生产、一换机器(比如从 x86 迁到 ARM)、一开编译器优化,就偶发地"见鬼"。99.99% 的执行都正常,剩下的 0.01% 足够你排查一周。看懂这期视频,你就拿到了理解这类诡异 bug 的"底层地图"——知道鬼从哪来,才不怕鬼。

视频背景卡片

项目 内容
视频标题 Why Multi-Threaded Code Can Sometimes Misbehave (Weak Memory Concurrency)
频道 Computerphile(百万级订阅的计算机科普频道,Jane Street 长期赞助)
主讲人 Prof. Azalea Raad(帝国理工学院 Veritas Lab 负责人,程序分析/并发验证方向)
发布日期 2026-05-18
时长 16 分 24 秒
播放量 约 6.9 万(截至 2026-08-30),点赞 3000+
核心主题 弱内存模型:为什么多线程程序会违反你对执行顺序的直觉

说明一句:这期视频单期播放量不算爆款,但它是少见的"由一线研究者用白板+现场跑代码讲透一个硬核主题"的内容,信息密度远高于一般教程。Computerphile 的出品质量有口皆碑,这一期尤其值得精读。

核心内容拆解

1. 一个 4 条指令的程序,颠覆你的直觉(视频原内容)

视频用了一个并发领域最经典的"试金石"程序(litmus test),俗称消息传递(Message Passing)模式:

问题:a 和 b 同时为 0 可能吗?

直觉说不可能。要么"x=1 先生效",那线程 2 的读 x 应该看到 1;要么"y=1 先生效",那线程 1 的读 y 应该看到 1。两边都读到 0,等于两个写操作互相"失联"了。

但 Azalea Raad 现场用 C++ 写了这个程序,循环执行直到出现 a=b=0——528 次迭代后出现了。这不是理论推演,是你今天的笔记本上就能复现的事实。

我的延伸:这个例子在并发验证领域几乎是"圣经级"的入门案例,学术文献里常叫 MP(message passing)测试。她现场跑出的"528 次"这个数字每次都会变(非确定性),但"必然出现"这个结论是确定的——这正是并发 bug 最恐怖的地方:不是不出现,而是不知道什么时候出现。

2. Lamport 的顺序一致性:那个"美好但过时"的世界(视频原内容)

要理解现实为什么"出错",先要理解我们以为的规则。视频介绍了图灵奖得主 Leslie Lamport 提出的顺序一致性(Sequential Consistency, SC)模型,两条规则:

  1. 每个线程内部的指令,按程序顺序执行;
  2. 不同线程之间的指令,可以任意交错(interleaving)。

在这个模型下,把所有可能的交错枚举一遍,a=b=0 确实不可能出现。这个模型简单、直观,几十年来一直是教学和推理的基石——Lamport 1979 年那篇经典论文的标题就是《How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs》。

我的延伸:Lamport 后来还发明了 TLA+(用于分布式系统形式化验证),和这期视频的主题一脉相承——都是"用数学模型约束并发行为"。顺带一提,SC 在分布式系统里依然被当作正确性的黄金标准(比如线性一致性 Linearizability 就比 SC 更强),只是 CPU 层面早已"背叛"了它。

3. 元凶一:编译器会偷偷重排你的指令(视频原内容)

第一个"鬼"来自编译器。为了优化性能,编译器有权在不改变"单线程语义"的前提下重排指令。在这个例子里,编译器完全可能把线程 1 的"读 y"提到"写 x"前面——因为单线程看,这两条指令没有数据依赖。

一旦重排发生,即使在纯 SC 模型下,a=b=0 也变得可能了:线程 1 先读 y(还是 0),线程 2 写 y=1、读 x(还是 0),最后线程 1 才写 x=1。两次读取都落在初始值上。

我的延伸:这就是 C/C++ 里"data race 是未定义行为(UB)"的根源之一——语言标准只保证单线程语义,没有同步的多线程访问共享变量,编译器和 CPU 可以"合谋"做出任何事。Rust 之所以敢宣称 "fearless concurrency",本质上就是用类型系统(Send/Sync)在编译期把这类未同步访问挡下来。

4. 元凶二:CPU 的 store buffer(视频原内容)

第二个"鬼"藏在硬件里,这也是视频讲得最精彩的部分。Azalea Raad 画了一个 x86 的简化模型:

于是"见鬼现场"出现了:线程 1 写 x=1(进自己的 buffer,还没到内存),线程 2 写 y=1(同样在 buffer 里),然后线程 1 读 y——自己 buffer 里没有 y,去内存读,读到 0;线程 2 读 x——同理读到 0。两个写都"执行了",但彼此都没看见。

我的延伸:这套机制有正式名字,叫 x86-TSO(Total Store Order)内存模型。x86 已经算是"很强"的内存模型了——它只允许"写后读"(store-load)这一种重排。ARM 更弱,允许的重排种类更多。这也是为什么很多在 x86 上跑了十年的代码,迁到 Apple Silicon 或 ARM 服务器上突然出并发 bug——不是 ARM 有问题,是 x86 一直"宠着"你。

5. 更诡异的四线程案例:连"全局顺序"都可以没有(视频原内容)

如果说双线程的例子还能靠 store buffer 解释,接下来的四线程例子(学术界叫 IRIW:Independent Reads of Independent Writes)更反直觉:

直觉上,两个读线程即便看到写入的顺序有先后,也应该"一致":要么都认为 x 先生效,要么都认为 y 先生效。不可能线程 2 看到"x=1、y=0",同时线程 3 看到"y=1、x=0"——这两组观察互相矛盾,说明系统对"写入的全局顺序"没有共识。

关键结论:x86 和现代 ARM 上这个结果不可能出现(它们保证 multi-copy atomicity,即所有核心以相同顺序看到写入);但在 IBM POWER 架构上,这是真实允许的行为。Azalea Raad 还提到一个行业八卦式的细节:ARM 早期规范其实也允许这种弱行为,后来发现没人用这个自由度,干脆收紧了规范。

我的延伸:这个案例说明内存模型不是物理定律,而是架构设计的商业与技术权衡。POWER 当年允许 IRIW 是为了给多核互连留优化空间,结果生态用脚投票,行业整体在向"更强(更直观)的内存模型"收敛。学并发的人必须记住:你的"直觉"来自哪一代硬件,并不天然适用于下一代。

6. 分布式系统的神类比:Facebook 墙上的生日祝福(视频原内容)

视频里我个人最喜欢的部分,是 Azalea Raad 用一个分布式系统的例子解释"为什么要允许弱行为":

假设两个用户同时在你 Facebook 墙上留生日祝福。对其他用户来说,这两条留言谁先谁后根本不重要。如果系统强行要求"全球所有用户必须以相同顺序看到这两条留言",实现成本极高;而允许每个数据中心的复制顺序不同(比如按物理距离就近传播),实现简单得多,用户体验毫无差别。

这就是弱内存模型存在的哲学:一致性是有代价的,凡是不需要强一致的地方,就应该允许弱一致来换性能。

我的延伸:这个类比把 CPU 微架构和分布式系统打通了——CPU 的每个核心本质上就是一个"数据中心",store buffer 就是"本地副本",缓存一致性协议(如 MESI)就是"副本同步协议"。理解了这一点,你再看 Kafka 的 ISR、Redis 主从复制、CRDT 这些分布式概念,会有完全不同的体感:它们是同一种思想在不同尺度上的重复出现。

7. 抽象层级定律:经典物理与量子物理(视频原内容)

视频结尾给了一个漂亮的收束。Azalea Raad 给学生讲课时的类比是:

我的延伸:这个类比的价值在于给出了行动指南——"我需要关心弱内存模型吗?"答案取决于你所在的抽象层。写 Python/Java 业务代码?Java 内存模型(JMM)已经帮你料理好了(前提是你正确使用 volatile/锁)。写 C/C++/Rust、写内核、写驱动、写 lock-free 数据结构?那这套知识就是必修课,不是选修课。

知识结构图

graph TD
    A[多线程并发程序] --> B{用什么模型理解?}
    B --> C[顺序一致性 SC<br/>Lamport 1979]
    B --> D[弱内存模型<br/>现实世界]

    C --> C1[线程内按序执行]
    C --> C2[线程间任意交错]
    C --> C3[结论: a=b=0 不可能]

    D --> E[为什么现实偏离 SC?]
    E --> F[编译器优化<br/>指令重排]
    E --> G[硬件优化<br/>store buffer 私有缓冲]

    D --> H[各架构的'弱'程度]
    H --> I[x86-TSO<br/>仅允许 store-load 重排<br/>强/直观]
    H --> J[现代 ARM<br/>重排更多 + multi-copy atomic]
    H --> K[IBM POWER<br/>允许 IRIW 反例<br/>最弱]

    D --> L[如何自保?]
    L --> M[高层: 依赖 OS/运行时<br/>锁与高级并发原语]
    L --> N[低层: 显式内存序<br/>acquire/release/seq_cst]
    L --> O[工具: TSan 等检测器<br/>形式化验证 litmus test]

    C3 -.被现实打脸.-> D

实战启示

看完视频,你能带走什么?按抽象层级给你三份"行动清单":

如果你写 C/C++/Rust 系统代码:第一,把"数据竞争 = 未定义行为"刻进 DNA——两个线程无同步地访问同一内存位置且其中一个是写,程序就已经非法了,不存在"竞争但结果还行"的侥幸。第二,用同步原语时想清楚内存序:Rust 原子操作里那个 Ordering::SeqCst 不是装饰,C++ 的 memory_order_acquire/release 是在跟 CPU 讨价还价——你用多强的顺序承诺,换多大的性能。第三,永远不要在没有 litmus test 验证的情况下手写 lock-free 结构。

如果你是后端/基础设施工程师:在 CI 里加上 ThreadSanitizer(C/C++/Rust 都支持)跑核心路径,竞争检测的误报率低到值得付出这点编译开销。跨平台迁移(x86 → ARM,尤其苹果硅和高通服务器芯片大迁移的当下)时,把"并发代码在 ARM 上重测"写进迁移 checklist——在 x86 上"碰巧正确"的代码,在 ARM 上是"碰巧暴露"。

如果你是学生或教学者:这期视频本身就是极佳的教学素材。配套练习可以这样做——在 Godbolt 上对比 -O0 和 -O2 的汇编输出看编译器重排;写一个 MP litmus test 循环跑到出现 (0,0),记录迭代次数分布;有余力的去读 herd7 这类内存模型模拟器的文档,把视频里的每个例子在模拟器里跑一遍。

最后一个认知升级:并发 bug 的"偶发性"不是 bug 的属性,而是概率的属性。528 次迭代出现一次,意味着百万次请求里它可能出现几十次——你的"稳定复现"和"无法复现"之间,只差一个负载量级。

延伸学习路径

  1. 该视频的姊妹篇:Computerphile 同月发布的《The "Trick" that Compilers Use for Long Division》(Matt Godbolt 主讲),从另一个角度展示编译器如何"自作主张"优化你的代码,和本文主题互为印证。
  2. 论文入门:Lamport 1979 年的《How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs》(IEEE Transactions on Computers),顺序一致性的原始出处,4 页纸讲清所有大思想。
  3. 体系结构教材:《A Primer on Memory Consistency and Cache Coherence》(Sorin, Hill, Wood 著),内存模型与缓存一致性协议的标准教材,第二版可免费获取。
  4. 语言层面的落地:C++ 标准的 memory model 章节(或 cppreference 的 memory_order 页面)、Rust 官方文档的 Atomics 章节(The Rustonomicon 相关章节),看语言如何把硬件模型封装成可用 API。
  5. 动手工具:内存模型模拟工具 herd7(剑桥/帝国理工一线研究组生态)和 Google ThreadSanitizer 文档,把视频里的例子亲手跑一遍。

一句话总结

你的多线程代码从来没有按你以为的顺序执行过——看懂编译器重排和 store buffer 这两只"看不见的手",才算真正拿到了并发的驾驶执照。

参考来源

  1. Why Multi-Threaded Code Can Sometimes Misbehave (Weak Memory Concurrency) — Computerphile — 本文分析的核心视频,含完整字幕
  2. Azalea Raad — Imperial College London 个人主页 — 主讲人及其 Veritas Lab 研究方向
  3. Computerphile 频道 — 系列计算机科普视频,本文延伸阅读的姊妹篇均出于此
  4. Lamport, L. (1979). How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs. IEEE Transactions on Computers — 顺序一致性原始论文(按标题检索可得)
  5. Brady Haran 博客 — Computerphile 背后的制作团队说明

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入