深入理解一致性与 C++ 内存模型

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

1. Consistency model

678c0c5b59d5c4e403896b7f40a8305d.png

上图摘自 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) 从特性上描述满足如下条件,则满足线性一致性:

  1. 满足 real-time,即在 invocation 和 reponse 之间产生影响。

  2. 具有原子性,不会比如因为 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 观察到的历史:

4e3c6077b05885c2a2f877d8d5603670.png 9e4c063c3dd3f91fe16c0f4fb34b5239.png

其中,I 代表 invocation,R 代表 response,A、B 代表不同的线程,1、2 代表不同的对象。

根据 wikipedia 定义中的规则,我们可以 reorder 出下面几个符合规则的调度历史:

7261892580931edf36ff020187369a6c.png

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

cc9594f2a374f3fea53888a0d3f15bc2.png

由于 RB1 在 IA2 之前,所以这样 reorder 破坏了规则:

如果在原始的历史中一个响应在另一个调用的前面,那在重排序的顺序历史中也必须同样如此。

那么现在有了这些,我们怎么判定 H_C 是一个正确的历史?答案就是只要任何一个根据 reorder 规则得到的调度历史是正确的(如果满足程序的语义 sequential definition of the object or semantics of the program),则 H_C 就是一个正确的历史,这个调度就是线性化的。进一步,如果一个系统所有的历史都是线性化的,则这个系统是满足线性一致性的

上述的描述还是有一点太抽象,这里给出直观的解释

  1. 满足程序序:reorder 的时候不会破坏具有时序关系的操作的顺序,但是对于并发的操作 reorder 的时候顺序任意,因为并发了本身结果就是无法预测,进而怎么 reorder 都可以。

  2. 对于 reorder 之后得到的调度历史,只要有任何一个满足程序的语义,则可以认为这个调度就是正确的。为什么任何一个正确就可以认为实际的 real-time 历史是正确的?可以分为两个方面:

  1. 没有并发。按照 reorder 规则,非并发的由于不会破坏 real-time 的 操作顺序,那么 reorder 之后只会有一个 real-time 的实际调度,这个调度的正确性就是代表了实际 real-time 历史的正确性。

  2. 有并发。按照 reorder 规则,并发的部分才可以任意 reorder,所以 reorder 的所有历史都只是不同并发 reorder 之后的组合得到的结果集,并发本身就是不确定的执行序,只要 reorder 得到的历史有任何一个正确,那么就可以认为实际执行就是这个顺序,也就是这个正确的顺序。

下面 wikipedia 给出的例子可以更为直观的理解这件事,两个线程 A 和 B 去拿同一个锁,执行的 real-time 实际历史 H_C 如下:

cf322fdd9a346853af9af8aca2a20f49.png

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

f8532176dc9dc76de8ee6d211f752c85.png

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

ccd585f24fc83ce76066f970fc4b566f.png

这个历史 H_S2 是符合程序语义的,B 先抢锁成功拿到锁,A 再抢锁拿锁自然会失败。因此基于 H_S2 reorder 正确这样的事实,得到实际执行满足线性一致性。

现在根据上面提到的直观的解释来理解这件事:根据 real-time 实际历史可以看出,A 和 B 在并发争锁,所以谁先谁后是不确定的,因为并发,所以也就能 reorder 出 2 个 结果了,并发本身执行序就不可确定,所以怎么解释都是可以的,又因为第 2 个 reorder 的历史是满足正确调度的程序语义的,则可以作为并发执行过程的解释,所以实际历史满足线性一致性。

顺序一致性 Sequential consi

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值