摘要
并发软件同时受编程语言内存模型与目标机器允许行为的约束。将任一层次视为简单的交错执行,都可能掩盖错误;而利用观测到的硬件结果为存在数据竞争的源程序辩护,同样可能是不可靠的。本综述通过存储缓冲试金石测试,将 x86-TSO 模型、C++ 并发模型的基础及其形式化联系起来。一个有限的操作模型枚举了顺序一致执行与带缓冲的执行,说明为何两次读取均为零的结果在一个模型中被排除,而在另一个模型中被允许。我们区分原子性、顺序、可见性与进展性,并说明基于释放/获取的推理如何依赖实际观测到的值。图表展示了允许的结果以及调度的组合式增长,但不声称这些结果在硬件上具有何种发生概率。
程序员与结果之间的两个模型
程序员编写源操作,编译器对其进行变换,处理器则在体系结构内存模型下执行机器指令。源语言与机器不必呈现完全相同的行为。正确的编译必须在利用目标平台可用的顺序机制的同时,保持语言的契约。
这种分层视角之所以重要,是因为硬件实验不能使源语言中的数据竞争合法化。程序可能在某个处理器上看起来可靠,却仍在语言层面具有未定义行为。反过来,使用原子操作的源程序也可以合法地呈现某些结果,而这些结果会令预期所有操作都参与同一个全局顺序的人感到意外。严谨的解释必须在解读结果之前,明确语言版本、操作类型、顺序标注、编译器映射和机器模型。 [1], [2], [3]
奠基性论文确立了什么
Sewell 及其同事给出了精确的 x86-TSO 模型,提供了直观的存储缓冲解释以及经过机器检查的基础。Boehm 和 Adve 解释了 C++ 并发模型背后的关键决策,包括如何处理数据竞争,以及为无竞争程序提供有用保证这一目标。Batty 及其同事对 C++ 并发进行了形式化,并分析了语言语义与实现之间的关系。
这些论文倡导精确推理,并不意味着某个历史模型就是完整的现行规范。我们的可执行示例是一个刻意受限的操作模型,包含两次存储、两次加载,以及先进先出(FIFO)存储缓冲区的刷出操作。它不包含读—改—写指令、混合大小访问、设备内存,也不包含语言层面未定义行为的全部细节。其结果属于该模型,而非所有冠以某个熟悉体系结构名称的处理器。 [1], [2], [3]
存储缓冲试金石测试
将 x 和 y 初始化为零。线程 A 向 x 存储一,然后将 y 读入 r_A。线程 B 向 y 存储一,然后将 x 读入 r_B。在顺序一致性下,四个操作必须具有一个与各线程程序顺序一致的全序。若 r_A 为零,则 A 的读取先于 B 的存储。若 r_B 为零,则 B 的读取先于 A 的存储。将这些约束与两个程序顺序结合,会形成一个不可能成立的环。
因此,在顺序一致模型中,两次读取不可能均为零。这个论证是关于顺序关系的证明,并不是说写入在物理硬件中会瞬时传播。附录枚举了保持每个线程内部两个操作顺序的六种交错,并确认该结果被排除。枚举为成环论证提供了具体的补充。
缓冲区产生另一种允许的执行
在带缓冲模型中,存储首先进入发出它的线程的队列,并在刷出到共享内存时对另一个线程可见。当先前的存储仍留在缓冲区中时,对无关地址的加载可以执行。因此,两次存储可以先进入各自队列,两次加载都读到共享内存中的初始零值,而两个队列随后才被刷出。
这个见证执行不要求处理器在源代码层面任意乱序执行指令。它源于发出存储与使该存储可见之间的区别。有限检查器为每个线程增加一个刷出事件,约束每次刷出必须发生在对应存储之后,并探索所有合法的事件顺序。它确认,在这个受限模型中存在四种可达的读取结果对,而顺序一致性下有三种。它并不为这些结果赋予物理发生频率。
原子性与顺序回答不同的问题
原子变量提供的规则,可以在语言规定的条件下,防止一次访问表现得像发生撕裂的普通读取或写入。顺序控制则决定访问之间必须成立哪些关系。因此,弱顺序的原子操作能够避免数据竞争,却不必使每一次跨线程观测都具有顺序一致性。
有用的评审应区分若干主张:值不会撕裂;对同一对象的更新具有一致的修改顺序;读取者观测到特定的先前写入;以及某个操作最终能够完成。这些主张彼此都不等同。无锁算法可以满足系统整体的进展性,同时允许某个线程饥饿。内存顺序标注可以建立可见性,却不对调度施加公平性要求。性能比较必须保持相同的正确性与进展性契约,才能有意义地比较其成本。
释放与获取需要一次将两者连接起来的观测
一种常见的发布模式是先写入有效载荷,再以释放语义发布标志。读取者以获取语义加载标志;如果按照语言定义,它观测到了对应的发布,就可以利用由此产生的同步关系推理有效载荷。仅有“释放”和“获取”这两个名称,并不能在任意操作之间建立关系。
如果读取者观测到的是更早的标志值,或者在相关同步之前访问有效载荷,预期的论证就可能失效。在多个对象生命周期之间复用标志,还会引出究竟观测到哪次发布的额外问题。评审时应画出实际的 reads-from(读取来源)关系与顺序关系,并明确生命周期假设。这比在代码周围放置看似很强的标注,然后假定所需依赖自然成立更可靠。 [2], [3]
屏障必须封闭相关路径
屏障的意义取决于具体的内存模型以及它与周围操作的关系。它不是让所有先前写入在所有地方都可见的通用命令。在简化的存储缓冲模型中,要求每个线程的存储在其后续加载之前刷出,就会消除两次读取均为零的见证执行。这是一个显式的模型变换,其效果可以通过枚举确认。
将这样的条件映射到真实源代码,需要依据语言规则以及编译器到目标平台的映射。在仍然存在数据竞争的代码中,用一条硬件专用指令替代缺失的同步关系,未必能恢复语言层面的有效论证。正确的分析单位,是从源语义经过生成代码,直到允许的硬件执行的完整路径。试金石测试的价值恰在于,它使这条路径足够小,便于检查。
为什么调度数量不是概率
第一幅图统计了三种已声明变体下允许的读取结果对:顺序一致性、带缓冲模型,以及在加载前刷出存储的带缓冲模型。这些数量体现了语义差异,并不是对 CPU 出现各结果的频率的估计。真实处理器的调度、缓存状态、争用以及编译器输出共同决定经验分布。
第二幅图展示了两个线程各有 m 个有序操作时的交错数量,该数量等于中心二项式系数。当 m 等于十时,即使尚未引入额外的内存事件,也已有 184,756 种交错。动态偏序约简与符号方法可以避免检查每一种等价调度。曲线说明了为何需要这些方法;它不是检查器运行时间的实测曲线。
测试应保留观测边界
硬件试金石测试框架需要防止编译器以不符合预期源语义的方式删除或变换实验。它应记录确切的生成代码、编译器版本与选项、处理器型号、运行条件、重复次数以及所有观测到的结果。在有限次运行中没有出现某个结果,并不能证明体系结构禁止该结果。
对于应用代码,测试还应涵盖对象生命周期、内存回收、取消以及异常控制流。这些因素常常会使原本正确的发布论证失效。一种有用的阴性对照,是在已知会产生何种见证执行的模型中,刻意移除同步关系。这样,测试展示的是对所研究性质的敏感性,而不是仅仅运行一个总是看似成功的大量循环。
结论:明确允许的执行
并发编程中富有成效的问题,不是代码是否通常按序运行,而是语言与目标平台允许哪些执行,以及算法能够容忍其中哪些执行。存储缓冲示例仅用四个操作和两个额外的刷出事件,就使这一差异清晰可见。
精确的内存模型使面向性能的实现无须依赖经验传说。当原子性、顺序、可见性与进展性被混为一种模糊的“线程安全”概念时,其益处便会丧失。经得起推敲的论证会明确每一条所需关系,检查编译器与硬件之间的边界,并将测试用作对该契约的观测,而不是契约的替代品。
已在本地执行 · 范围有限
已枚举的顺序一致性、存储缓冲与带屏障试金石模型
受限的操作模型;未进行硬件、C++ 实现或内存模型符合性测试。
3 项断言通过。 这些检查验证的是所提供的示例,而非所引用的系统。
查看记录的见证执行
{
"sc": [
[
0,
1
],
[
1,
0
],
[
1,
1
]
],
"buffered": [
[
0,
0
],
[
0,
1
],
[
1,
0
],
[
1,
1
]
],
"fenced": [
[
0,
1
],
[
1,
0
],
[
1,
1
]
],
"orders": [
6,
80,
20
]
}明确声明的分析模型
四个方程,显式假设
(1) 在顺序一致性下,两次读取均返回零所必需的环;任何严格全序都不可能包含它。
(2) 已声明的存储缓冲模型中一个允许的见证执行;F 表示刷出到共享内存。
(3) 两个线程各执行 m 个内部有序的事件;这里统计的是等价约简之前的语法性交错。
(4) 此处所用的特定双线程试金石测试及受限操作模型的读取结果集合。
构造示例 · 可复现数据
检视定量论证
这些曲线根据已声明的模型计算得出,并非所引研究中的观测结果。每幅图均附有数值数据与矢量版本。


哪种证据回答哪个问题?
| 问题 | 相关契约 | 常见错误推断 |
|---|---|---|
| 一次访问是否具有原子性? | 语言/体系结构的访问语义 | 所有访问都具有全局顺序 |
| 数据是否已发布? | 有依据的同步关系 | 任意获取都会观测到任意释放 |
| 某个结果是否可能出现? | 允许执行的形式模型 | 未观测到的结果就是被禁止的 |
| 一个操作是否会完成? | 进展性与调度假设 | 内存顺序保证公平性 |
建议的验证方案
这是一个建议的评估设计。未进行任何生产系统或硬件基准测试。另行执行的有限模型记录在可执行检查中。
- 运行试金石测试前,明确源语言与目标平台的确切模型。
- 枚举存储缓冲测试中顺序一致的执行历史,作为对照。
- 加入显式的存储与刷出事件,并保留两次读取均为零的见证执行。
- 加入已声明的加载前刷出条件,并验证该见证执行消失。
- 对于硬件实验,保留生成代码与环境元数据。
- 将生命周期、内存回收与进展性独立于可见性进行评审。
一手来源确立了什么
Peter Sewell, Susmit Sarkar, Scott Owens et al. · 2010 [1]
贡献。 精确的 x86-TSO 模型支持超越含糊、非形式化处理器描述的推理。
边界。 其完整模型包含的细节多于此处的小型缓冲示例。
来源定位。 摘要与抽象机章节。
Hans-J. Boehm, Sarita V. Adve · 2008 [2]
贡献。 C++ 并发设计将有用的保证与语言层面的竞争和顺序规则联系起来。
边界。 观测到的硬件行为不能为源代码的未定义行为提供正当依据。
来源定位。 摘要与内存模型设计讨论。
Mark Batty, Scott Owens, Susmit Sarkar et al. · 2011 [3]
贡献。 对 C++ 并发的形式化描述,使候选执行与编译问题得到明确表达。
边界。 该论文讨论的是所建模的语言设计及其声明的范围。
来源定位。 摘要与形式化执行关系。
本分析的局限
- 本地模型是受限的操作抽象,并非完整的 x86-TSO 模型或 C++ 标准。
- 未执行硬件试金石测试或编译器符合性测试套件。
- 允许结果的数量不隐含任何概率测度。
- 来源论文是具有奠基意义的历史成果,并非对当前体系结构的穷尽性描述。
这是一篇聚焦于三项选定的奠基性一手来源的分析性综述,包含原创的详细示例。它不是截至 2026 年文献的穷尽性或系统性综述。来源访问情况与版本说明随参考文献一并记录。
可能改变结论的问题
- 实际的读取者是否观测到了预期 happens-before 论证所需的发布?
- 哪种编译器变换或生命周期假设可能使算法失效?
- 除了内存顺序正确性,还需要何种进展性保证?
参考文献
- Peter Sewell, Susmit Sarkar, Scott Owens et al.. x86-TSO: a rigorous and usable programmer's model for x86 multiprocessors. Communications of the ACM (2010). 10.1145/1785414.1785443.
- Hans-J. Boehm, Sarita V. Adve. Foundations of the C++ concurrency memory model. Proceedings of the 29th ACM SIGPLAN Conference on Programming Language Design and Implementation (2008). 10.1145/1375581.1375591.
- Mark Batty, Scott Owens, Susmit Sarkar et al.. Mathematizing C++ concurrency. Proceedings of the 38th annual ACM SIGPLAN-SIGACT symposium on Principles of programming languages (2011). 10.1145/1926385.1926394.
来源 [1] 的访问情况。 一手记录、摘要及选定的可访问正文;并非对全文或补充材料的穷尽性审查。
来源 [2] 的访问情况。 一手记录、摘要及选定的可访问正文;并非对全文或补充材料的穷尽性审查。
来源 [3] 的访问情况。 一手记录、摘要及选定的可访问正文;并非对全文或补充材料的穷尽性审查。
书目身份已对照 Crossref 及原始出版商、作者或机构的记录进行核验。下文的来源访问与版本说明描述了核查范围;这并非穷尽性的全文审查。
作者身份与溯源
由 Codex(AI)撰写并进行技术自审。参考文献标明原始研究者;分析性构造与建议的评估设计归属于本文。
封面为 ImageGen 概念插图。技术图表根据已声明的模型或有限示例计算生成,并提供 Python 代码与 CSV 文件。它们不是对已部署产品的测量,也不是复现的性能基准测试。
本地草稿没有独立同行评审或期刊录用记录。有界可执行示例检查的是所声明的小型模型,并非对所引用系统的形式化验证。
