先看一个让我后背发凉的视频片段:伦敦帝国理工学院的 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 的出品质量有口皆碑,这一期尤其值得精读。
视频用了一个并发领域最经典的"试金石"程序(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 最恐怖的地方:不是不出现,而是不知道什么时候出现。
要理解现实为什么"出错",先要理解我们以为的规则。视频介绍了图灵奖得主 Leslie Lamport 提出的顺序一致性(Sequential Consistency, SC)模型,两条规则:
在这个模型下,把所有可能的交错枚举一遍,a=b=0 确实不可能出现。这个模型简单、直观,几十年来一直是教学和推理的基石——Lamport 1979 年那篇经典论文的标题就是《How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs》。
我的延伸:Lamport 后来还发明了 TLA+(用于分布式系统形式化验证),和这期视频的主题一脉相承——都是"用数学模型约束并发行为"。顺带一提,SC 在分布式系统里依然被当作正确性的黄金标准(比如线性一致性 Linearizability 就比 SC 更强),只是 CPU 层面早已"背叛"了它。
第一个"鬼"来自编译器。为了优化性能,编译器有权在不改变"单线程语义"的前提下重排指令。在这个例子里,编译器完全可能把线程 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)在编译期把这类未同步访问挡下来。
第二个"鬼"藏在硬件里,这也是视频讲得最精彩的部分。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 一直"宠着"你。
如果说双线程的例子还能靠 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 是为了给多核互连留优化空间,结果生态用脚投票,行业整体在向"更强(更直观)的内存模型"收敛。学并发的人必须记住:你的"直觉"来自哪一代硬件,并不天然适用于下一代。
视频里我个人最喜欢的部分,是 Azalea Raad 用一个分布式系统的例子解释"为什么要允许弱行为":
假设两个用户同时在你 Facebook 墙上留生日祝福。对其他用户来说,这两条留言谁先谁后根本不重要。如果系统强行要求"全球所有用户必须以相同顺序看到这两条留言",实现成本极高;而允许每个数据中心的复制顺序不同(比如按物理距离就近传播),实现简单得多,用户体验毫无差别。
这就是弱内存模型存在的哲学:一致性是有代价的,凡是不需要强一致的地方,就应该允许弱一致来换性能。
我的延伸:这个类比把 CPU 微架构和分布式系统打通了——CPU 的每个核心本质上就是一个"数据中心",store buffer 就是"本地副本",缓存一致性协议(如 MESI)就是"副本同步协议"。理解了这一点,你再看 Kafka 的 ISR、Redis 主从复制、CRDT 这些分布式概念,会有完全不同的体感:它们是同一种思想在不同尺度上的重复出现。
视频结尾给了一个漂亮的收束。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 次迭代出现一次,意味着百万次请求里它可能出现几十次——你的"稳定复现"和"无法复现"之间,只差一个负载量级。
你的多线程代码从来没有按你以为的顺序执行过——看懂编译器重排和 store buffer 这两只"看不见的手",才算真正拿到了并发的驾驶执照。
本文由「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 等场景全覆盖。
