关注公众号获取更多信息:
上一篇文章讲atomic的时候,曾经涉及到内存模型,atomic的默认类型是:memory_order_seq_cst。这个东东是啥?
其实它是内存模型的一种。下面我们来详细讲解一下C++11内存模型,读完这篇文章,你就能弄懂它的意思了。注意,这篇文章可能偏理论性质,也不会有例子,如果想快速入门可以直接看下一篇文章。
1. 内存模型的所有种类:
typedef enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
} memory_order;
2. WHY - C++11的内存模型有六种。那么为什么要有内存模型呢?
下面是官网的解释:
std::memory_order 指定内存访问,包括常规的非原子内存访问,如何围绕原子操作排序。在没有任何制约的多处理器系统上,多个线程同时读或写数个变量时,一个线程能观测到变量值更改的顺序不同于另一个线程写它们的顺序。其实,更改的顺序甚至能在多个读取线程间相异。一些类似的效果还能在单处理器系统上出现,因为内存模型允许编译器变换。
库中所有原子操作的默认行为提供序列一致顺序(见后述讨论)。该默认行为可能有损性能,不过可以给予库的原子操作额外的 std::memory_order 参数,以指定附加制约,在原子性外,编译器和处理器还必须强制该操作。
3. Prerequisite - 讲内存模型前,先要了解一些基础知识
线程间同步和内存顺序决定表达式的求值和副效应如何在不同的执行线程间排序。它们用下列术语定义:
先序于
在同一线程中,求值 A 可以先序于求值 B ,如求值顺序中所描述。
携带依赖
在同一线程中,若下列任一为真,则先序于求值 B 的求值 A 可能也会将依赖带入 B (即 B 依赖于 A )
1) A 的值被用作 B 的运算数,除了
a) B 是对 std::kill_dependency 的调用
b) A是内建 && 、 || 、 ?: 或 , 运算符的左运算数。
2) A 写入标量对象 M,B 从 M 读取
3) A 将依赖携带入另一求值 X ,而 X 将依赖携带入 B
修改顺序
对任何特定的原子变量的修改,以限定于此一原子变量的单独全序出现。
对所有原子操作保证下列四个要求:
1) 写写连贯:若修改某原子对象 M 的求值 A (写操作)先发生于修改 M 的求值 B ,则 A 在 M 的修改顺序中早于 B 出现。
2) 读读连贯:若某原子对象 M 的值计算 A (读操作)先发生于对 M 的值计算 B ,且 A 的值来自对 M 的写操作 X ,则 B 的值要么是 X 所存储的值,要么是在 M 的修改顺序中后于 X 出现的 M 上的副效应 Y 所存储的值。
3) 读写连贯:若某原子对象 M 的值计算 A (读操作)先发生于 M 上的操作 B (写操作),则 A 的值来自 M 的修改顺序中早于 B 出现的副效应 X (写操作)。
4) 写读连贯:若原子对象 M 上的副效应 X (写操作)先发生于 M 的值计算 B (读操作),则求值 B 应从 X 或从 M 的修改顺序中后随 X 的副效应 Y 取得其值。
释放序列
在原子对象 M 上执行一次释放操作 A 之后, M 的修改顺序的最长连续子序列由下列内容组成
1) 由执行 A 的同一线程所执行的写操作
2) 任何线程对 M 的原子的读-修改-写操作被称为以 A 为首的释放序列。
依赖先序于
在线程间,若下列任一为真,则求值 A 依赖先序于求值 B
1) A 在某原子对象 M 上进行释放操作,而不同的线程中, B 在同一原子对象 M 上进行消费操作,而 B 读取 A 所引领的释放序列的任何部分 (C++20 前)所写入的值。
2) A 依赖先序于 X 且 X 携带依赖到 B 。
线程间先发生于
在线程间,若下列任一为真,则求值 A 线程间先发生于求值 B
1) A 同步于 B
2) A 依赖先序于 B
3) A 同步于某求值 X ,而 X 先序于 B
4) A 先序于某求值 X ,而 X 线程间先发生于 B
5) A 线程间先发生于某求值 X ,而 X 线程间先发生于 B
先发生于
无关乎线程,若下列任一为真,则求值 A 先发生于求值 B :
1) A 先序于 B
2) A 线程间先发生于 B
要求实现确保先发生于关系是非循环的,若有必要则引入额外的同步(若引入消费操作,它才可能为必要,见 Batty 等)。
若一次求值修改一个内存位置,而其他求值读或修改同一内存位置,且至少一个求值不是原子操作,则程序的行为未定义(程序有数据竞争),除非这两个求值之间存在先发生于关系。
简单先发生于 无关乎线程,若下列之一为真,则求值 A 简单先发生于求值 B : 1) A 先序于 B 2) A 同步于 B 3) A 简单先发生于 X ,而 X 简单先发生于 B 注:不计消费操作,则简单先发生于与强先发生于关系是相同的。 | (C++20 起) |
强先发生于
无关乎线程,若下列之一为真,则求值 A 强先发生于求值 B :
1) A 先序于 B 2) A 同步于 B 3) A 强先发生于 X ,而 X 强先发生于 B | (C++20 前) |
1) A 先序于 B 2) A 同步于 B ,且 A 与 B 均为序列一致的原子操作 3) A 先序于 X , X 简单先发生于 Y ,而 Y 先序于 B 4) A 强先发生于 X ,而 X 强先发生于 B 注:非正式而言,若 A 强先发生于 B ,则在所有环境中 A 均显得在 B 之前得到求值。 注:强先发生于排除消费操作。 | (C++20 起) |
可见副效应
若下列皆为真,则标量 M 上的副效应 A (写入)相对于 M 上的值计算(读取)可见:
1) A 先发生于 B
2) 没有其他对 M 的副效应 X 满足 A 先发生于 X 且 X 先发生于 B
若副效应 A 相对于值计算 B 可见,则修改顺序中,满足 B 不先发生于它的对 M 的副效应的最长相接子集,被称为副效应的可见序列。( B 所确定的 M 的值,将是这些副效应之一所存储的值)
注意:线程间同步可归结为避免数据竞争(通过建立先发生于关系),及定义在何种条件下哪些副效应成为可见。
消费操作
带 memory_order_consume 或更强标签的原子存储是消费操作。注意 std::atomic_thread_fence 会施加比消费操作更强的同步要求。
获得操作
带 memory_order_acquire 或更强标签的原子存储是获得操作。互斥体 (Mutex) 上的 lock() 操作亦为获得操作。注意 std::atomic_thread_fence 会施加比获得操作更强的同步要求。
释放操作
带 memory_order_release 或更强标签的原子存储是释放操作。互斥体 (Mutex) 上的 unlock() 操作亦为释放操作。注意 std::atomic_thread_fence 会施加比释放操作更强的同步要求。
今天就到这里吧,下篇会详细的讲解每个内存序的作用。