惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Azure Blog
Microsoft Azure Blog
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
V
V2EX
PCI Perspectives
PCI Perspectives
Latest news
Latest news
博客园 - 三生石上(FineUI控件)
C
CERT Recently Published Vulnerability Notes
W
WeLiveSecurity
Last Week in AI
Last Week in AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Palo Alto Networks Blog
T
The Exploit Database - CXSecurity.com
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
WordPress大学
WordPress大学
V
Vulnerabilities – Threatpost
H
Heimdal Security Blog
Attack and Defense Labs
Attack and Defense Labs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Hacker News: Ask HN
Hacker News: Ask HN
博客园 - 叶小钗
V
Visual Studio Blog
Jina AI
Jina AI
P
Proofpoint News Feed
罗磊的独立博客
SecWiki News
SecWiki News
J
Java Code Geeks
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
LINUX DO - 热门话题
Security Archives - TechRepublic
Security Archives - TechRepublic
The Hacker News
The Hacker News
Hugging Face - Blog
Hugging Face - Blog
N
News and Events Feed by Topic
NISL@THU
NISL@THU
T
Tailwind CSS Blog
T
Tenable Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Recent Announcements
Recent Announcements
H
Hacker News: Front Page
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Tor Project blog
宝玉的分享
宝玉的分享
Help Net Security
Help Net Security
S
Security Affairs
Microsoft Security Blog
Microsoft Security Blog
Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
G
GRAHAM CLULEY

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 分享一下笔者的 Mac 装机必备软件 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
[A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型
SudosuBash · 2026-04-17 · via 博客园_首页

[A Primer Of CC And MC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型

前言

最近去了杭州的金沙湖玩, 顺带分享几张照片, 正好最近不太开心, 也算缓解了一下自己的情绪吧.

0

0

0

好了, 书接上回。上回我们对内存一致性协议起了个头, 这回我们继续来看内存一致性.

不过, 在开始对 内存一致性 的探索之前, 我们得先反向思维一下, 探究一下内存不一致的情况, 看看究竟在什么情况下, 这种"内存不一致"的情况会出现呢?

CRUX PROBLEM - 关键问题

  • 既然我们知道了内存一致性, 那么内存不一致性体现在哪 4 大重排序?
  • 为了规范这 4 大重排序, 我们引入了哪个内存一致性模型去约束它们?
  • Sequential Consistency 的定义是什么? 这个内存一致性模型解决了这 4 大重排序中的哪些重排序呢?

0 一些符号的预先说明

为了方便后续的讲解, 我们先进行一些规定. 这部分在书上都有, 也有对应的图片.

设:

  • L(a) 代表指令 a 的读取, S(a) 代表指令 a 的写入
  • 我们把 L(a)S(a) 暂时统一叫做 P(a)
  • P(a) <m P(b) 为 a 指令的实际退休顺序比 b 指令前面
  • P(a) <p P(b) 为 a 指令的程序顺序比 b 指令前面

1 "四大天王": CPU 的指令重排序

1.1 Load-Load 重排序

首先,我们先来看一张图:

2

Woah, 是不是特别熟悉, 在哪见过? 对啦, 就在前一份随笔中,我们遇见过它, 当时我稍微简略的说了, 这个就是 load-load 重排序. 现在我们就来仔细研究一下.

这段代码的逻辑很简单, 就是 C2 在只有 flag = SET 的时候, 才获取 r2 的值.

为了方便说明, 假设我们的 CPU 可以对指令进行重排序, 那么我们首先来看一下 C2 的指令:

; Core C2
L1:
2: load r1, [flag]
3: cmp r1, SET
4: jne L1
5: load r2, [data]

看看这段代码, 就看 C2 的代码(C1暂时先撂一边, 这很重要!), r1 和 r2 有没有关系? 回答我, look in my eyes!

没关系对吧? 那我现在是一个 CPU, 我把 5 的指令排到 2 上面可不可以? 当然可以!

如果读取 data 的速度比读取 flag 还快的话, 咱把 5 的指令挪到 2 的上面就行了, 然后就形成了如下局面:

Tick C2指令
2 load r2, [data]
3 load r1, [flag]
4 ...

这重排序没有任何问题, 但是它能完成最初我们想让 CPU 干的事吗?

这段代码的逻辑很简单, 就是 C2 在只有 [flag] = SET 的时候, 才获取 r2 的值.

不行了对不对? 程序乱了! 那这为什么不对呢? 我们来理理线索:

  • 将 C1 和 C2 连起来看, 变量 data 和变量 flag 顺序很明显是不能变的
  • 单独只看 C2, 变量 data 和变量 flag 没半毛钱关系
  • CPU 能在保证单核心数据正确性的情况下对单核的指令进行重排序.

注意: 这儿的重排序乱序执行的区别是很大的, 但是若你理解了 SC 是抽象层/契约的话, 这个就不难理解了.

  • 前者指的是 CPU 在保证单核最终结果正确的情况下, 重新排序 CPU 的最终指令执行顺序, 也就是说, 前者直接打破了这个契约, 在程序员眼中, CPU 的指令执行顺序是不确定的.
  • 后者是 CPU 的流水线后端所干的事. CPU 完全可以维持 SC 这一契约, 也可以选择不维持. 而至于程序员眼中,CPU 是否遵循 SC,和 CPU 是否乱序执行, 没有一点关系, 因为遵循 SC 的 CPU 照样可以做到乱序执行(至于怎么做到的, 下一章讲).

进一步地, 我们可以继续概括出 load-load 触发的条件:

  • CPU 具有单核指令重排序的功能.
  • 存在多个在局部(单个核心)无关的变量, 但是它们在全局(例如多个核心)逻辑中是有刚性的顺序要求的(上面的 data 和 flag 就是一个典型的例子).

注意, 下面术语是描述此重排序不允许发生时候的定义.

用书上的话/上面约定的符号: if L(a) <p L(b), then L(a) <m L(b)

1.2 Store-Store 重排序

在此之前, 我们先来粗略聊聊 CPU 的 Load/Store Buffer.

续接上集的 1.1, 核内缓存确实解决了一些速度问题, 但是, 这事还没完, 因为要保证多核数据一致性, 缓存加了一个 Interconnection Network 层, 如下图, 用来在缓存之间传递数据, 还有就是管理缓存的权限.

注: 读到这, 肯定会有读者跳出来说, 你讲错了,Directory 才是 CPU 真正管理缓存权限的地方. 我知道你很急, 但你先别急, 后续会提到的.

2

这层加进来后, 在核心数量少的时候其实也不影响啥, 但是核心数量一多, 诶, 这层的电路就越来越复杂, 但这不是最重要的. 更重要的是, 它啊! 你想想, 要 CPU 在执行访存指令的时候还得同步等一下缓存之间的, 那性能没救了, 直接重开吧.

诶, 注意我加粗的文字, 同步! 那既然同步的性能会一言难尽, 那就改一下思路--异步!

所以啊, 现在的 CPU 核心就整了一个叫做 Load/Store Buffer 的东西. 正好这玩意可以和 CPU 的推测执行打配合:

  • CPU 可以先预测接下来需要读写的内存单元, 然后在 Load/Store Buffer 上先大笔一挥记上去, 就类似给缓存发了一个请求, 然后立刻就去乱序执行下一条预测代码.

  • 如果是读取的话, Load/Store Buffer 在后台先搞到数据(如果是读的话), 然后等到 CPU 确认这条指令必须执行后, 再执行最终写入(寄存器).

  • 如果是写入的话, Load/Store Buffer 会先提前申请到修改权限, 然后等到 CPU 确认这条指令必须执行后, 再执行最终写入(缓存).

注: 目前我说的这些是简化的模型, 肯定有读者跳出来说, 你这模型太简单了. 我知道你很急, 但是你先别急. 目前, 我们暂时规定, 访存指令一经退休, 所执行的操作立马全局可见.

我们来看一个例子, 还是上面的那张图:

1

这段代码的逻辑很简单, 就是 C2 在只有 [flag] = SET 的时候, 才获取 r2 的值, 目标是获取 [data] 的新值.

Tick C1 指令 C2 指令 说明
1 S1: store [data], NEW - S1 进入 Store Buffer
2 S2: store [flag], SET - S2 进入 Store Buffer
3 - load r1, [flag] S2 先写入完毕退休, r1 = SET
4 - cmp r1, SET r1 是否等于 SET?
5 - jne L1 通过比较, 往下走
6 - load r2, [data] r2 = [data], BOOM!
7 - - S1 才退休, [data] = NEW

看到了吗? 在 Tick 6 的时刻, C2 崩了, 完美避过正确答案, 它成功获取到了 data 的旧值.

那为什么不对呢? 我们来观察一下:

  • S1 和 S2 的指令是单核心的, 并且指定顺序的.
  • S1 和 S2 有顺序的进入 Store Buffer, 但是它们最终提交 (退休) 的顺序不一致.
  • 在 S1 和 S2 生效的间隙, 由于 jne 不成立, 所以有一个 load 指令来读取 data 中的内存数据.
  • CPU 能在保证单核心数据正确性的情况下对单核的指令进行重排序 (就是不按序退休).

进一步地, 我们可以继续概括出 store-store 触发的条件:

  • CPU 原本有多条在逻辑上强制指定顺序, 但是可以指令重排的 store 指令 (不管是单核心还是多核加锁变量).
  • 这些指令进入 Store Buffer 后, 并没有老老实实的按进入的顺序提交最终结果.
  • CPU 能在保证单核心数据正确性的情况下对单核的指令进行重排序 (就是不按序退休).

注意, 下面术语是描述此重排序不允许发生时候的定义.

用书上的话/上面约定的符号: if S(a) <p S(b), then S(a) <m S(b)

1.3 Load-Store 重排序

让我们继续来看一段代码, 假设此时: [x] = 0, [y] = 0, NEW != 0:

C1 指令 C2 指令
L1: load r1, [x] L2: load r2, [y]
S1: store [y], NEW S2: store [x], NEW

按照我们的直觉可以得出, 上述的表格有几种可能的执行顺序:

顺序 结果
L1->L2->S1->S2(S2->S1) r1 = 0, r2 = 0
L1->S1->L2->S2 r1 = 0, r2 = NEW
L2->L1->S1->S2(S2->S1) r1 = 0, r2 = 0
L2->S2->L1->S1 r1 = NEW, r2 = 0

but先别急, 加入 Load Buffer 后, 结果可能和你想的有点不一样, 我们以 L1->L2->S1->S2 的顺序再走一遍--

注意: 下面的内存赋值语句, 例如下面表格的 [x] = NEW, 它的含义是“内存值 [x] 更新为 NEW, 此操作对所有 CPU 核心可见”.

Tick C1 指令 C2 指令 实际发生的 我们预想的
1 L1: load r1, [x] - L1 进入 Load Buffer r1 = [x]
2 - L2: load r2, [y] L2 进入 Load Buffer r2 = [y]
3 S1: store [y], NEW - [y] = 0 [y] = NEW
4 - S2: store [x], NEW [x] = 0 [x] = NEW
5 - - L1 退休, r1 = [x] -
6 - - L2 退休, r2 = [y] -

现在我们来看一下最终的结果, 容易得出:

  • r1 = NEW
  • r2 = NEW

诶, 由于 Load Buffer 在这捣乱, 这种情况可不在上面可能执行顺序的结果内啊!

问题出在哪呢? 我们概括一下:

  • 在逻辑顺序上, L1 比 S1 先执行
  • 但是在实际执行中, L1 却比 S1 后退休

那我们从具体到泛化一下, 假设有一条读取指令 A, 另一条写入指令 B, 出现下面的情况:

  • 逻辑上的顺序为 A-B
  • 诶, 很不巧, 由于 Load Buffer 或者其他乱七八糟的因素, 实际的提交顺序却是 B-A
  • 也就是说, A 在提交值之前, B 就已经开始读取值了, 所以 B 才会读取到错误的值.

这种逻辑上应该是读取指令 A 先, 写入指令 B 后, 但是实际执行顺序却是 B 先 A 后, 导致 A 读取到错误数据的情况, 我们叫做 Load-Store 重排序.

注意, 下面术语是描述此重排序不允许发生时候的定义.

用书上的话/上面约定的符号: if L(a) <p S(b), then L(a) <m S(b)

1.4 Store-Load 重排序

我们现在把 1.3 中的代码反一下, 假设此时: [x] = 0, [y] = 0, NEW != 0:

3

同上, 这张图有几种可能的执行顺序:

顺序 结果
S1->S2->L1->L2(L2->L1) r1 = NEW, r2 = NEW
S1->L1->S2->L2 r1 = 0, r2 = NEW
S2->S1->L1->L2(L2->L2) r1 = NEW, r2 = NEW
S2->L2->S1->L1 r1 = NEW, r2 = 0

上述很符合我们的逻辑, 对吧? 毕竟按照我们人类的线性思维, 这样可再正常不过了.

同上, 加入 Store Buffer 后, 我们以 S1->S2->L1->L2 的顺序再走一遍--

Tick C1 指令 C2 指令 实际发生的 我们预想的
1 S1: store [x], NEW - S1 进入 Store Buffer [x] = NEW
2 - S2: store [y], NEW S2 进入 Store Buffer [y] = NEW
3 L1: load r1, [y] - r1 = [y] = 0 r1 = [y] = NEW
4 - L1: load r2, [x] r2 = [x] = 0 r2 = [x] = NEW
5 - - S1 退休, [x] = NEW -
6 - - S2 退休, [y] = NEW -

现在我们来看一下最终的结果, 容易得出:

  • r1 = 0
  • r2 = 0

原因同上, 不再赘述. 这儿直接摆出 Store-Load 的定义:
这种逻辑上应该是写入指令 A 先, 读取指令 B 后, 但是实际执行顺序却是 B 先 A 后, 导致 B 读取到错误数据的情况, 我们叫做 Store-Load 重排序.

注意, 下面术语是描述此重排序不允许发生时候的定义.

用书上的话/上面约定的符号: if S(a) <p L(b), then S(a) <m L(b)

还有一点小插曲--

若不知道 TSO 内存一致性模型是什么的读者, 可以暂时先跳过此部分. 此部分专门讲述, 为什么 TSO 允许产生 Store-Load 重排序.

在这个例子中, 我们可以很轻易地注意到, Store-Load 重排序:

  1. 并没有涉及到对指令顺序的重排序.
  2. 不依靠 Store Buffer 中的记录顺序(这儿先挖个坑qwq, 一种比较常见的 Store Buffer 的记录顺序叫做 FIFO, 也就是先进先出).
  3. 本质是由于 Store Buffer 的延迟, 导致读取指令 A 在写入指令 B 提交之前读取数据.

TSO 模型只能保证每个核心的 Store Buffer 是 FIFO 的, 从而避免了 Store-Store 重排序.

然而, TSO 并不能保证每条读取指令都得等到另外一条写入指令最终提交后再读取值, 正如上述的例子那样.

所以, TSO 照样有可能产生 Store-Load 重排序.

那有读者会问, Load-Store 重排序也有上述特性, TSO 是怎么解决的呢? 答案可能让你出乎意料--TSO 中, Store Buffer 在写入前有一个硬性规定, 若 Load Buffer 非空, 则 Store Buffer 不会提交, 换句话说, Store 指令必须得等 Load 指令全部提交完, 才会去提交.

but, 随之而来又有一个问题--- 为什么 TSO 允许 Store-Load 而不是 Load-Store?

原因其实很简单, 一个字--. 如果是 Store Buffer 等待 Load Buffer, 那么 CPU 就可以通过 Load Buffer 疯狂的执行预测, 和进行下一步的计算. 要是 Load Buffer 等待 Store Buffer 的话... 好问题, 啥预测之类的全得排 Store 后边, 那 CPU 会慢的像蜗牛一样.

并且, 要是 Load 指令预测错误, 就算 Load 退休了, 也就影响一个核心, 大不了这个核心把错误的值部扔了, 没毛病. 但是 Store 要是退休了... 那影响的可就不止一个核心了.

2 最符合直觉的模型: SC (Sequential Consistency)

2.1 抽象: 再探内存一致性模型

还记得我们前一篇文章写的 3 个抽象层级吗? 现在, 我们修改一下抽象层级 1, 让它更符合程序员眼中的 CPU 的样子:

抽象层级 1: 程序员眼中: 哦,CPU 提供了访存指令 mov/ldr/sto, 并且 CPU 执行这些指令的顺序是规定好的(有序/无序).

什么意思呢? 也就是说, 在程序员的视角中, CPU 可以被抽象成一个按一定顺序执行指令的电子元器件. 但是别忘了这是在程序员的视角! 在 CPU 内部, 这些顺序执行的指令是完全可以做到乱序执行的.

我们只需要保证, CPU 执行指令的所有可能的最终结果, 是按该内存一致性顺序执行后所得的所有可能的结果的其中之一 (不论是 Weak Memory 模型, 还是 TSO 模型, 或者是现在的 SC), 我们就称它为 遵循 SC(这儿的 SC 换成具体的名字) 模型.

这句话可能有点绕, 一个最直观的例子就是上面的 Load-Store, 按照我们按顺序执行指令的直觉(其实我们的直觉就是 SC 内存一致性模型), 不管指令按什么可能的顺序排列, r1 = NEW, r2 = NEW 的结果都是不可能出现的, 但是它却出现了. 我们把它叫做违反 SC(这儿的 SC 换成具体的名字) 模型.

所以, 内存一致性模型本质上是一套和程序员的规定, 也就是说, 内存一致性模型规定了在程序员眼中, CPU 实际到底会以怎样的顺序处理和排序他们写的指令.

打个比方:

例如餐厅叫号系统, 我们付钱后拿到一个排队顺序, 然后只需要等待排队就好了. 但是, 至于内部的厨师是先做 1 号顾客的菜, 还是先做 2 号顾客的菜, 我们不需要知道, 我们只需要知道, 最终的顺序是先 1 号取餐, 再轮到 2 号. 然后餐厅对外宣传: 我们采用了智能叫号系统, 按顺序取餐.

现在, 厨房就是 CPU 流水线后端, 顾客就是指令, 海报就是内存一致性协议, 看到这个海报的人的视角就是程序员视角. 看海报的人只需要知道这个餐厅是按序取餐就好了, 没有必要知道厨房内部的具体的做饭顺序. 但是, 厨房员工采用了乱序制作的手段加快做菜速度.

2.2 解决: 我们先试试 SC

还记得我在 1 中讲的 3.3 部分吗? 当时, 我们讲了内存一致性模型, 它告诉 CPU 或者程序员, 该以什么样的顺序执行指令, 才能保证 CPU 最终数据的正确性. 而我们还讨论到, CPU 的乱序执行会导致数据错乱. 而导致数据错乱的原因, 主要就是上面 4 种形式的重排序.

要解决这个问题, 我们必须建立一套指令之间内存读写的秩序, 并且还是用到万能的权衡大法, 以下是两个极端的方案:

  1. CPU 为了速度, 疯狂对指令重排序, 但是它提供了一系列控制指令, 程序员可以通过指令对 CPU 说: 嘿, 将这条指令作为程序的分界线, 界限下面的指令不能被乱序到到界限上面去执行, 否则出大问题 (这样灵活, 没得说, 并且能少用一堆逻辑门).

  2. 重新设计 CPU 的执行引擎电路, 给 CPU 加点小小的魔法(好吧, 其实就是抽象层), 让程序员以为 CPU 真的在乖乖的按照程序员指定的顺序执行指令. 至于 CPU 如何实现这个魔法, 好吧, 其实程序员并不需要知道. (这样子能让程序员少掉点头发, 妙哉妙哉).

现代 CPU 的内存一致性方案, 其实就是这两个方案经适当权衡之后的结果. 而这套叫 Sequential Consistency (SC, 顺序执行) 的模型, 可谓对程序员是极其友好, 它就是极端方案的第二种: 程序员完全可以认为 CPU 真的在按照程序员指定的顺序去执行指令, 而不会为了速度任性地重排序它们.

例如在之前的例子中:

L1:
I2: load r1, [flag]
I3: cmp r1, SET
I4: jne L1
I5: load r2, [data]

在程序员眼中, CPU 的指令执行顺序为 I2->I3->I4->I5, 那 CPU 最终给程序员呈现的结果就是正经的去执行 I2->I3->I4->I5 后的结果, 不会因为出现上述的"四大天王"重排序, 而导致乱七八糟的问题.

用官方的术语定义 SC:

  • if S(a) <p S(b), then S(a) <m S(b)
  • if L(a) <p L(b), then L(a) <m L(b)
  • if L(a) <p S(b), then L(a) <m S(b)
  • if S(a) <p L(b), then S(a) <m L(b)

由此可见, SC 把 4 大重排序全都干掉了. 也正因为它严格顺序执行, 所以它是程序员的福音, 能让程序员就按照他自己想的方式去写代码, 不必担心 CPU 发生指令重排.

2.3 易混淆的点: 执行路径和 SC

在单核心中, SC 似乎很好理解, 毕竟, SC 就是顺序执行嘛. 但是在多核心中, 情况似乎有所不同了.

2.3.1 多条 SC 的合法路径

敲黑板啦! 多核心下的 SC 是有很多很多条执行路径的, 就类似我们之前研究的并发编程一样. 例如之前的例子中:

顺序 结果
S1->S2->L1->L2(L2->L1) r1 = NEW, r2 = NEW
S1->L1->S2->L2 r1 = 0, r2 = NEW
S2->S1->L1->L2(L2->L2) r1 = NEW, r2 = NEW
S2->L2->S1->L1 r1 = NEW, r2 = 0

别看这些例子看上去挺乱的不可预测, 但是它们都是符合 SC 模型的, 因为它们都是按照一定的顺序集合起来的. 但是, 如果某个时刻 CPU 计算出来的结果并不为右边的值的其中之一, 那这个 CPU 才是违反 SC的.

若有一个 SC 模型的 CPU 执行你的并发代码后, 出现了一些很诡异的结果, 这个时候先别急着把 CPU 抠下来然后往地上摔(虽然这确实挺恼火的, 这是人之常情), 我建议你先喝杯咖啡冷静一下, 然后拿一张纸和一支笔, 在纸上重新推一下这段并发代码, 看看是不是某些竞争条件被遗漏了.

2.3.2 SC 执行过程的偏差和执行结果的合法

注意: 以下我在讨论的是执行路径, 不是 CPU 本身. CPU 本身违反 SC 不假, 但是我现在在探讨的是执行路径是否违反 SC.

现在, 我问一个极其刁钻的问题, 基于上述的情境, 假设有如下执行的路径:

Tick C1 指令 C2 指令 实际发生的 我们预想的
1 S1: store [x], NEW - S1 进入 Store Buffer [x] = NEW
2 - S2: store [y], NEW S2 进入 Store Buffer [y] = NEW
3 L1: load r1, [y] - r1 = [y] = 0 r1 = [y] = NEW
4 - - S1 退休, [x] = NEW -
5 - - S2 退休, [y] = NEW -
6 - L2: load r2, [x] r2 = [x] = NEW r2 = [x] = NEW

问题: 我设置的这条执行路径:

  1. 它存在 Store-Load 重排序吗?
  2. 它是否符合 SC?

(先思考 5 秒钟... 想到了吗?)

答案:

  1. 很明显的, 它存在 Store-Load 重排序, 因为指令 L1 在 S1 执行完毕之前读取了值.
  2. 见下

分析上述例子的执行路径, 我们可以看到, 虽然有:

路径 结果
S1->S2->L1->L2 r1 = 0, r2 = NEW

但是, 它却并不违反 SC, 为什么呢? 在此之前, 我再问一个问题: 这条执行路径, 它位于我们之前所讲的抽象层级的第几层?

(再思考 5 秒钟...)

答案是第二层. 对, 它属于 CPU 微架构和流水线层面. 但是, 我们需要记住, 我们是站在程序员的视角下看问题的, 也就是位于抽象层级的最顶端(第一层), 我们是不知道, 也不需要知道 CPU 的微架构和流水线长什么样的.

在程序员的视角下, 我们看到的是这样的指令:

3

假设执行上述代码的 CPU 遵循 SC, 那上述执行链条, 所得到的结果有可能是

  • r1 = NEW, r2 = NEW
  • r1 = 0, r2 = NEW
  • r1 = NEW, r2 = 0

r1 = 0, r2 = NEW 在不在这里面? 在啊! 没毛病啊! 我们在不知道具体执行路径的情况下, 完全可以构造一条可能合理的解释路径, 去解释这个结果.

换句话说, 其实看执行链是否遵循 SC, 就只需要看它结果就行了, 然后怎么推到这个结果的? 自己猜去, 天知地不知, 你不知我不知.

The End

SC 的含义是,CPU 营造了一个假象, 让程序员们以为 CPU 真的在按照程序员指定的顺序去执行指令, 而不会为了速度任性地重排序它们.

一个遵循 SC 模型的 CPU, 只需要保证 CPU 执行指令的所有可能的最终结果, 是按该内存一致性顺序执行后所得的所有可能的结果的其中之一, 否则就是违反 SC.

由于篇幅有限, SC 的实现部分先挖个坑.

本期文章写到这, 感谢大家的观看哦~萌新初涉系统编程, 有错误也请多多指正~

版权声明: 本文采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
作者: Sudo-su-Bush
发布时间: 2026-04-17
原文链接: https://www.cnblogs.com/SudosuBash/p/19884395