C C++最全深入理解一致性与 C++ 内存模型,小红书C C++面试题目

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 终态可串行化

2a56492f6ff15fc751bbf74797935e3b.png

对于一个历史 s,如果存在一个串行的历史 s’ 和 s 终态等价,则称历史 s 是终态可串行化的。用 FSR 表示所有终态可串行化的调度历史聚类。

总结

终态可串行化实际没有应用的,且不说终态等价的验证复杂度,从定义可以看出对于终态的等价性上并没有考虑只读事务的终态,而这会导致读不一致异常的发生:

e1cd86556a46c99ca949ef27b4784cfc.png

视图可串行化 View Serializability

既然终态可串行化没有考虑读会导致等价关系不完整进而会出现异常,那么想办法让整个验证完整不就行了么?视图可串行化就是这样的背景产生的,视图可串行化会验证调度中每一个步骤的等价关系。

  • 视图等价

46c31e3b427a5268dbb7e61b30f46873.png

其中 Hs§ 表示 p 的 Herbrand 语义,简单理解就是 操作的上下文和作用效果。

简单来说,对于两个调度 s 和 s’,如果满足终态等价的要求之外,还要满足 过程中每一个操作的 Herbrand 语义(上下文和作用效果) 相同,则 s 和 s’ 是视图等价的。

  • 视图可串行化

173352df5aaee438aff360506067e098.png

对于一个历史 s,如果存在一个串行的历史 s’ 和 s 视图等价,则称历史 s 是视图可串行化的。用 VSR 表示所有视图可串行化的调度历史聚类。

由于 视图等价 基于 终态等价 的等价关系之外额外附加了调价,所以可以直观的了解到:VSR ⊂ FSR。

总结

由定义可以看出,视图可串行化要求非常严谨,会要求整个事务过程的所有过程必须等价,这导致验证的开销会非常大,所以实际中没法应用。验证调度是否是视图可串行化的是一个 NP complete 问题:

2b9a90d1b07bc37f75d9c7063e9f0417.png

冲突可串行化 Conflict Serializability

更进一步,引起调度是非串行化的根本原因是事务的操作之间有 read-write 或者 write-write 冲突,无冲突的部分互相无关,不会对彼此造成影响。因此基于这个出发点,就有了相比视图等价进一步缩小约束范围的冲突等价性,即只考虑冲突部分的等价关系。

58d4ee16ac89bc11288b4019ac3fcc0c.png

简单来说就是在一个调度 s 的两个事务中,如果它们有操作相同的数据项并且至少有一个是写操作,则认为这两个事务中的操作构成冲突关系,s 中所有的冲突表示为 conf(s)。

  • 冲突等价

b288008f9060571a528b9d51be62f333.png

对于两个调度 s 和 s’,如果他们的所有操作相同,并且有相同的冲突关系,则 s 和 s’ 是冲突等价的。

  • 冲突可串行化

88401e763121b1712187509664e303ba.png

对于一个历史 s,如果存在一个串行的历史 s’ 和 s 冲突等价,则称历史 s 是冲突可串行化的。用 CSR 表示所有冲突可串行化的调度历史聚类。由于只考虑冲突部分,可以直观的了解到:CSR ⊂ VSR ⊂ FSR。

总结

冲突可串行化是主流数据库系统中实现串行化隔离级别最为常见的模型。一般通过 Two-Phase Locking Protocol 来实现,另外主流数据库也都是 SS2PL 的方式来实现,这一点可以参考 Wikipedia Two-Phase Locking。

严格可串行化 Strict Serializability

通过上述对可串行化的定义可以看出,只要求等价性,并没有 real-time 的约束,也就是说任意事务可以任意乱序。比如对于 T1, T2 和 T3,T1 和 T2 有并发,T3 时序上在 T1 和 T2 之后,前面提到的可串行化可以调度成最终结果等价于 T3 -> T1 -> T2 这样一个违背 real-time 的串行历史。

严格可串行化相比前面的可串行化,多了 real-time 的约束,所以对上述例子的调度结果只可能是等价于 T1 -> T2 -> T3 或者 T2 -> T1 -> T3。所以严格可串行化也是一个在可串行化概念上添加 real-time 额外约束衍生出来的概念。更多的理解可以参见下面外部一致性部分的内容。

读未提交 Read Uncommitted(Dirty)

一个事务读到了另一个进行中事务的中间结果。实现比如读和写的锁都仅限于读写的时刻,用完之后立刻释放。一般情况下应该没有人用这种隔离级别,这基本相当于没有事务,那也就没必要使用事务了,这里只是介绍一下概念。

ba18c813c6901f4c120a2d0ef2265996.png

读已提交 Read Committed(RC)

一个事务执行过程中可以读到另一个已提交事务的数据。存在不可重复读和幻读异常:事务进行中读到了不一致的数据。现实比如读的时候加锁但完成之后立刻释放,写的时候加锁且直到事务提交才释放。

a900d329efbb6fa3c8f63e7ce9ce154b.png

可重复读 Repeatable read(RR)

一个事务执行过程中不会读到本事务具体行的其他事务提交的数据。存在幻读异常:事务进行中读到了不一致的数据。现实比如读写都加锁且持续到事务结束,但并没有加表锁,不相关的行还是可以变更。

ccb59384155d3176504cfa6880c990ae.png

快照隔离 Snapshot Isolation(SI)

事务是满足快照隔离的,不存在不可重复读和幻读。存在写偏斜(WriteSkew)异常:读和写之间没有并发控制。

3dbeb878341e8b7c5f0a2120755bb897.png

一般化的隔离级别

由于 ANSI 提出的隔离级别比较老旧,不太能表达乐观锁和 MVCC 的情况,有人提出了更为精准的隔离级别的分类方法,这方面可以参考论文:《Generalized Isolation Level Definitions》。

1.3 外部一致性 External Consistency

外部一致性是一个相对泛泛的概念,可以分为 2 个方面:

  1. 用在非事务操作上,则等价于 Linearizability。

  2. 用在事务操作上,则类似于 Strict Serializability,比 Serializability 要强,相当于事务级别的 Linearizability,给事务调度加了 real-time 的约束但是并不要求瞬时性(instantaneousness)。简单来说假如有这样一个场景:事务 T1 和 T2,用户先执行了 T1 且提交成功之后再执行 T2。对于 Serializability 来说,先 T1 再 T2 或者先 T2 再 T1 都是满足语义的;但是对于 Strict Serializability 来说看起来就必须是先 T1 再 T2。

注:对于 Google Spanner 来说,除了上面描述的之外,还蕴含着内部数据物理时间与外部物理时间一致的外部一致性。另外还有只读事务以快照的方式读任意副本都能够保证读取到最近快照的外部一致性。

2. C++ memory model


在第 1 章介绍了普遍意义下各个一致性模型的语义,那到了单机系统中,对于系统编程来说,面对着多核系统以及不同架构下的不同一致性语义,该如何理解不同内存模型下的一致性语义进而写出符合正确同步语义代码呢?本节要基于 C++11 定义的内存模型来展开讨论这个问题。

dc8b66a7e0f3723076e6b437f0821926.png

首先需要明确的是一个单机系统,其实也完全就是一个分布式系统。比如跨 NUMA 的内存访问就需要走总线,这就相当于网络;对于 IO 子系统(disk、net)的访问同样需要走各种总线;CPU 对内存的访问也一样是分布式的。我们知道,对于 CPU 来说,每一个 core 有自己的 L1 cache,cores 间还有 L2 cache 甚至 L3 cache,那么这些 cores 之间数据的一致性保证就是非常重要的,这有一个专门的概念叫 Cache Coherence,相应的协议就是 Cache Coherence Protocol,一般都是 MESI 及其扩展协议,定义了 cores 间的数据一致性模型,主要就是规范了第 1 章中提到的比如 strict、sequential、causal 等一致性模型在多 cores 之间如何满足。

为了追求性能,各种 CPU 架构尤其是 ARM 提供的一致性模型都是比较松散的,在多 cores 间默认可以理解为是达不到 causal consistency 的,不仅硬件一致性协议会导致一致性的不确定,CPU 的流水线优化、乱序执行以及编译器的优化都会导致一致性的不确定性,因此我们想做到 causal 甚至 sequential 一致性就需要通过一定的同步语义来做到。在 C++11 之前 C++ 标准并没有给出相关的标准化规范,linux 侧都需要通过 programmer 基于 GCC 或汇编下的原子操作和内存屏障来达到目的,好在 C++11 抽象了相应的内存模型,从语言层面提供了同步语义。

我们这里不需要深入了解 MESI 协议在不同 CPU 架构下是有怎样的一致性保证以及如何人为做同步的,有了 C++ 内存模型这一层抽象,我们只需要面向 C++ 内存模型去实现就好了。所以这里不会对 MESI 及其衍生协议进行介绍,对于 programmer 来说其实意义不是很大,另外也不是本文的主题。

下面首先介绍 C++ 中用于同步的语义以及相关的概念;之后具体的基于不同原子操作提供的语义介绍如何建立同步语义。其他语言也是如此,只需要面向具体语言规范来理解和实现即可

2.1 Concepts

58507f362567b2eb06a5431a1a20f211.png

上面是 C++ 官网给出的内存序枚举定义,由上到下由松散逐渐严格。可以看出最高的一致性保证是 memory_order_seq_cst,也就是 sequential consistency,所以可以看出 C++ 是不支持 strict 一致性的,也就是说任何写操作是没办法 real-time、瞬时完成立即可见的。基于 C++ 内存模型,参考第 1 章顺序一致性和因果一致性中提到的概念:

03e6a1e13d5e13cdeb3a376bddb01b6d.png 88897665fb1e674c611f375569c54a8d.png

可以看出,在一致性最高级别也不支持 strict,而且出于性能的考虑程序员也会尽量用宽松的一致性,那基于一定的结果进而就可以确定性地推断出一定的原因的能力就非常重要了,这是典型的因果关系,由于 sequential consistency 能提供比因果更强的保证,所以这里只基于因果关系进行讨论。

因果关系,最初是由 Lamport 在 《Time, Clocks, and the Ordering of Events in a Distributed System》paper 中提出的 happened-before 关系发展而来。Lamport 在 paper 中称之为 happened-before,但是 C++ 称之为 happens-before,为了统一描述在 C++ memory model 这个主题中后续均采用 happens-before 这一说法。在 C++ 中,说 A happens-before B,那么可以明确的就是时序上可以认为 A 先发生 B 后发生,B 可以观察到 A。因此只要我们在程序中建立了 happens-before 关系,那么就能得到因 happens-before 而来的同步关系。下面介绍 C++ 内存模型中 happens-before 相关的概念。

Sequenced-before

在同一线程中,evaluation A 根据语言定义的执行序 sequenced-before evaluation B。比如

5763a73e6aaaef2cda3c69ea5ed62ea4.png

具体规则参考 Order of evaluation。

Carries dependency

简单理解,在同一线程中,如果 A sequenced-before B 且 evaluation B 用到了 A,则 A carries dependency into B,即 B depends on A。

Modification order

对于单个变量的原子操作,无论以什么样的 memory_order,都是有全局序的,但是不同变量间的原子操作是没有这样的保证的,想要这种保证就需要有一定的同步语义或者完全使用 memory_order_seq_cst。

Modification order 指的就是单个对象上原子操作的全局序。可以看出,C++ 约束了对于同一个变量原子操作的 total order,其实就是具备 sequential consistency。

Release sequence

对于一个变量 M,evaluation A 是作用在 M 的一个 release operation,那么在 A 之后由下面两种情况:

  • 同一线程内以任意 memory_order 执行的原子操作,

  • 不同线程内 read-modify-write 类的所有原子操作,比如 fetch_add、cas 等

构成的 modification order(包括 A) 称之为 以 A 为 head 的 release sequence

比如对于 M,A 是一个 release operation,之后本线程内有 relaxed 的任意操作 B,其他线程有 read-modify-write 类操作如 fetch_add C、D,那么 A -> {B、C、D } 是以 A 为 head 的 release sequence。由于 B、C、D 顺序不确定,所以这里放到了一个大括号内。具体为什么这么设计,可以参考 <<C++ Concurrency in Action>> 5.3.4 一节。

Dependency-ordered before

在线程间,如果满足下面任一情况,则 evaluation A dependency-ordered before evaluation B:

  • A 在 M 上执行了一个 release operation,在另一个不同的线程,B 对 M 执行了一个 consume operation,并且 B 读了以 A 为 head 的 release sequence 中的任意一个操作的写入值。

  • A dependency-ordered before X 并且 X carries a dependency into B。

Synchronizes-with

在线程间,同一变量的一对具备 Release - Acquire 语的义原子操作会建立 同步。比如 evaluation A 是对 M 的 release operation,而之后 evaluation B 是对 M 的 acquire/consume operation,则称 A synchronizes-with B。根据 memory_order 中介绍的内容,能够在线程间构成 synchronizes-with 的 operations 如下:

  • Mutex 的 lock() 和 unlock()

  • 具有 Release-Acquire 语义的原子操作

  • 具有 Release-Acquire 语义的 fences

需要注意的是,memory_order_seq_cst 是包含 release & acquire 语义的,更多的细节参考 memory_order 文档即可。

Inter-thread happens-before

在线程间,如果满足下面任一情况,则 A Inter-thread happens-before B:

    1. A synchronizes-with B
    1. A is dependency-ordered before B
    1. A synchronizes-with some evaluation X, and X is sequenced-before B
    1. A is sequenced-before some evaluation X, and X inter-thread happens-before B
    1. A inter-thread happens-before some evaluation X, and X inter-thread happens-before B
Happens-before

如果满足下面任一情况,则 A happens-before B(以后偶尔会用 “->” 简化):

    1. 单线程内 A is sequenced-before B
    1. 多线程间 A inter-thread happens-before B

至此,我们由 sequenced-before 一步步走到了 happens-before 关系。因为 A happens-before B 则 A 对 B 可见,所以只要构建了 happens-before 关系就具备了同步能力。

需要注意的是,从文档可以看出 C++ 的 happens-before 强调的是 visible 性,而没有 execution order 的约束。比如如下例子:

7f8e0ba545d89c3b4a220a0e3641743a.png

我们考虑单线程执行的情况,对于上面的代码,A sequenced-before B,由于单线程考虑,因此 A happens-before B,但是实际执行时顺序可以是 B 先执行 A 后执行。可以看出,这并没有破坏 happens-before 对 visible 性的保证,最终结果是等价于 A 先执行 B 后执行,所以 B 先于 A 执行依然满足 happens-before 语义,但 execution order 不是 program order。

下面 2.2 节会结合具体的 memory_order 类型以及例子进一步强化理解。

2.2 Atomic Operations

memory_order_relaxed

宽松操作:没有同步或顺序约束,仅对此操作要求原子性。

memory_order_consume

可以与其他线程的 release operation 构成 synchronization。另外不仅如此,还具有一个 副作用(side effects,memory barrier 的作用) :所有本线程内依赖于该 memory_order_consume operation 得到的 value 的所有读或者写不可以重排到该操作之前。

memory_order_acquire

类似于 memory_order_consume 但是副作用强于 memory_order_consume,要求所有 memory_order_acquire 操作后面的读或者写都不可以重排到该操作之前。

memory_order_release

可以与其他线程的 acquire/consume operation 构成 synchronization,副作用是所有当前线程的读或者写不可以重排到该操作后面。具体的 synchronization 行为如下:

  • 与 acquire:当前线程的所有写入对具备 acquire operation 的其他线程可见。

  • 与 consume:当前线程所有 carry a dependency into memory_order_release 变量的写入对具备 consume operation 的其他线程可见。

memory_order_acq_rel

memory_order_acq_rel 只能作用在 read-modify-write 动作上,语义上是 memory_order_acquire 和 memory_order_release 的结合。

memory_order_seq_cst

不仅具有 memory_order_acquire、memory_order_release 和 memory_order_acq_rel 有的语义,额外保证所有 memory_order_seq_cst 作用的变量具备全局序。

2.3 Examples

这里的例子都以原子操作为基础,但是其实通过 std::atomic_thread_fence 结合 Release-Acquire 语义也一样可以实现同步。

Sequential

1ffd82caa8ad842b149ef8f56f5c2108.png

最终 assert 会通过。

直观的理解:由于 x 和 y 都是 memory_order_seq_cst memory_order,所以具有全局序。因此当 B 判定失败导致 z 没有 ++,那说明对于 read_x_then_y 来说,顺序是 x = true -> y = true -> x = true -> y = false,这个顺序对于 read_y_then_x 来说,一旦 C 通过,因为 x = true 在 D 之前,所以 D 也会判定通过,进而 z 会 ++,其他情况读者可以自行分析。另外,语义上的理解可以参考 <<C++ Concurrency in Action>> 的 5.3.3 一节。

Relaxed

4497e33964d6224203236f03047f2b54.png

上面的代码中,由于 x 和 y 都是 relaxed order,所以没有任何同步,因此 assert 可能失败。

直观的理解:relaxed 没有同步语义,因此 A、B、C、D 可以乱序,没法判定先后顺序。

语义上理解:如下图,不同线程中的 A 没有和 C 建立 synchronizes-with 关系,所以 也就没有 A happens-before C 的关系,所以 read_y_then_x 中 d 执行见到的 x 可能还是 false。

3a25d09cc4c6fa8d2bae6c4e19834dcb.png

Release-Acquire

fc67023d76a64548461a86e3279a9508.png

这个例子中,assert 判定可能会失败。不同于 memory_order_seq_cst,Release-Acquire 没有全局序,因此没办法按照 Sequential 例子中的语义做同步了。简单理解就是 x 和 y 没啥关系,随便乱序。

efb4ad4a7716967ee68edf4a81ef8e89.png

assert 不会失败。因为 B synchronizes-with C,因此 B -> C。又因为 A -> B,C -> D,所以 A -> B -> C -> D,所以 C 发生之候 x 一定为 true,因此 D 会判定通过。

3efccfc40f22d253515469577e5e616c.png

3. References


  • <>

  • https://en.wikipedia.org/wiki/Consistency_model

  • https://en.wikipedia.org/wiki/Linearizability

  • https://en.cppreference.com/w/cpp/atomic/memory_order

  • https://stackoverflow.com/questions/9762101/what-is-linearizability

  • https://timilearning.com/posts/consistency-models/

  • https://cloud.google.com/spanner/docs/true-time-external-consistency

  • https://timilearning.com/posts/mit-6.824/lecture-13-spanner/

  • https://jepsen.io/consistency

  • <<C++ Concurrency in Action>>

  • <>

  • http://www.bailis.org/blog/linearizability-versus-serializability/

  • http://www.cs.cornell.edu/courses/cs734/2000FA/cached%20papers/SessionGuaranteesPDIS_1.html#HEADING1

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • https://timilearning.com/posts/mit-6.824/lecture-13-spanner/

  • https://jepsen.io/consistency

  • <<C++ Concurrency in Action>>

  • <>

  • http://www.bailis.org/blog/linearizability-versus-serializability/

  • http://www.cs.cornell.edu/courses/cs734/2000FA/cached%20papers/SessionGuaranteesPDIS_1.html#HEADING1

[外链图片转存中…(img-VIvv9TKg-1715708351973)]
[外链图片转存中…(img-iMrawEFo-1715708351973)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值