本文旨在对计算机科学下的一致性模型以及 C++ 的内存模型做一个系统的、深入浅出的介绍。一共 3 个 章节:第 1 章介绍一致性模型,第 2 章介绍 C++ 内存模型,第 3 章是参考资料。
1. Consistency model

上图摘自 https://jepsen.io/consistency,这是一个对一致性比较好的分类和总结。
右面的分支表达了 operations 之间的顺序以及可见性,左面的分支表达了 operations 作为 group 时的特性,比如一组 operations 可能需要和另一组 operations 具有某种组级别的作为整体来看的一致性语义,一般来说就是事务性,要满足一定的完整性约束。下面分别就两个方向所涉及到的各种主要的一致性概念进行介绍。
1.1 Non Transaction
本节关于一致性的介绍是按照严格到放松的顺序来介绍的,在每一个一致性介绍的开始处都有相比较上一个一致性的说明,因此最好按照顺序去理解概念。
Data-centric consistency models
严格一致性 Strict consistency
严格一致性是最强的一致性保证,任意处理单元对变量的写入要求对所有处理单元立刻可见。
可以看出这样实现对性能的影响非常不好,需要有很大的通信开销来保证可见性,实际上基本没有实现这个一致性级别的系统。
线性一致性 Linearizability(Atomic consistency)
线性一致性不是一个很容易深入理解的简单概念,很多人都误会线性一致性为作用在一个对象上的概念,其实这么理解是错误的,线性一致性没有作用到一个对象上的约束,如开头分类时提到的,只不过没有对 operations 进行 group 行为(一般来讲就是事务性)的约束而已,是面向多对象下一般化场景的。线性一致性没有严格一致性要求的那么强,只是“看起来像”严格一致性,没有那么强的瞬时性(instantaneousness)要求。这里分两个方面介绍线性一致性:一个是个人的提炼直观理解,一个是 wikipedia 中的定义。
直观理解
对于一个系统如果所有 调用(Invocations) 和 响应(Responses) 从特性上描述满足如下条件,则满足线性一致性:
满足 real-time,即在 invocation 和 reponse 之间产生影响。
具有原子性,不会比如因为 client 自身的 retry 导致一个自增动作执行多次。
正式定义
Wikipedia 中更正式的概念是(解释翻译版,Wikipedia 词条中有原文)。
对于一个 invocations 和 responses 的历史 H,如果满足下面的条件则 H 就是线性化的:
对于 H 中完成的所有操作,返回的结果必须等价于某一个操作看成原子(invocation 得到 reponse 是立刻的) 的情况下构成的串行执行的历史。
实际执行过程中如果一个操作 op1 在另一个操作 op2 完成之前,那么在 H 中 op1 也必须在 op2 之前。
换句话说:
H 的所有 invocations 和 responses 可以被重排序成一个按顺序(串行)的历史;
如果满足程序的语义(sequential definition of the object or semantics of the program),则这个顺序历史就是正确的;
如果在原始的历史中一个响应在另一个调用的前面,那在重排序的顺序历史中也必须同样如此。
单看概念有点抽象,下面举个例子。假设有这样一个并发执行下的 real-time 观察到的历史:


其中,I 代表 invocation,R 代表 response,A、B 代表不同的线程,1、2 代表不同的对象。
根据 wikipedia 定义中的规则,我们可以 reorder 出下面几个符合规则的调度历史:

而下面这个调度历史就是不符合规则的:

由于 RB1 在 IA2 之前,所以这样 reorder 破坏了规则:
如果在原始的历史中一个响应在另一个调用的前面,那在重排序的顺序历史中也必须同样如此。
那么现在有了这些,我们怎么判定 H_C 是一个正确的历史?答案就是只要任何一个根据 reorder 规则得到的调度历史是正确的(如果满足程序的语义 sequential definition of the object or semantics of the program),则 H_C 就是一个正确的历史,这个调度就是线性化的。进一步,如果一个系统所有的历史都是线性化的,则这个系统是满足线性一致性的。
上述的描述还是有一点太抽象,这里给出直观的解释。
满足程序序:reorder 的时候不会破坏具有时序关系的操作的顺序,但是对于并发的操作 reorder 的时候顺序任意,因为并发了本身结果就是无法预测,进而怎么 reorder 都可以。
对于 reorder 之后得到的调度历史,只要有任何一个满足程序的语义,则可以认为这个调度就是正确的。为什么任何一个正确就可以认为实际的 real-time 历史是正确的?可以分为两个方面:
没有并发。按照 reorder 规则,非并发的由于不会破坏 real-time 的 操作顺序,那么 reorder 之后只会有一个 real-time 的实际调度,这个调度的正确性就是代表了实际 real-time 历史的正确性。
有并发。按照 reorder 规则,并发的部分才可以任意 reorder,所以 reorder 的所有历史都只是不同并发 reorder 之后的组合得到的结果集,并发本身就是不确定的执行序,只要 reorder 得到的历史有任何一个正确,那么就可以认为实际执行就是这个顺序,也就是这个正确的顺序。
下面 wikipedia 给出的例子可以更为直观的理解这件事,两个线程 A 和 B 去拿同一个锁,执行的 real-time 实际历史 H_C 如下:

根据 reorder 规则,可以有如下两个 reorder 的调度历史:

这个历史 H_S1 不符合程序语义,因为 A 先执行就肯定能拿到锁。

这个历史 H_S2 是符合程序语义的,B 先抢锁成功拿到锁,A 再抢锁拿锁自然会失败。因此基于 H_S2 reorder 正确这样的事实,得到实际执行满足线性一致性。
现在根据上面提到的直观的解释来理解这件事:根据 real-time 实际历史可以看出,A 和 B 在并发争锁,所以谁先谁后是不确定的,因为并发,所以也就能 reorder 出 2 个 结果了,并发本身执行序就不可确定,所以怎么解释都是可以的,又因为第 2 个 reorder 的历史是满足正确调度的程序语义的,则可以作为并发执行过程的解释,所以实际历史满足线性一致性。
顺序一致性 Sequential consi