C++11 内存模型详解(续)

在说到内存模型相关的东西时,我们常常会说到两个名词:乱序与可见性,且两者经常交错着使用,容易给人错觉仿佛是两个不同的东西,其实不是这样,他们只是从不同的角度来描述一个事情,本质是相同的。比如说,我们有如下代码:

atomic<int> g_payLoad = {0}; atomic<int> g_guard = {0}; // thread 0 void foo1() { g_payLoad.store(42, memory_order_relaxed); g_guard.store(1, memory_order_relaxed); } // thread 1 void foo2() { int r = g_guard.load(memory_order_relaxed); if (r) { r = g_payLoad.load(memory_order_relaxed); } }

因为 g_guard 与 g_payLoad 的读写都是以 relaxed 方式进行,因此我们会认为 foo2() 中 g_guard.load() 与 g_payLoad.load() 有可能会乱序(从不同的地址 load, cpu 可能会 speculate & prefetch,etc),但从另一个角度来看,g_payLoad 被 reorder 到 g_guard 之前(其它类型乱序同理),其实就相当于 g_payLoad 在 foo1 中的修改没有被 foo2 所看到,同一件事情的两个不同角度,如此而已。

但在处理 c++11 的内存模型时,语言标准上的措词却基本上是基于“可见性”来描述的(和 Java 的内存模型描述基本一致),看得多了,我也渐渐发现从“可见性”来理解问题有时真会容易很多,不信请继续看。

Happen-before

Happen-before 指的是程序指令间的一种关系,且是一种运行时的关系,是动态的,如果指令 A happen-before B, 则 A 的副作用能够被 B 所看到,容易理解对吧?且暂时可简单认为 happen-before 具有传递性(transitive)(准确地说是在不考虑 consume ordering的情况下), 如果 A happen-before B, B happen-before C,则我们可以认为 A happen-before C。
两条指令 A 与 B 如要满足 happen-before 关系,必须满足如下条件之一:

A sequence-before B: 这个指的是在同一个线程里两条指令的关系,从程序源码上看如果指令 A 在 B 之前,则 A sequence before B, 比如前面代码中 g_payLoad.store() 这一行代码就是 sequence-before g_guard.store().

A synchronize-before B: 这个指的是两个线程间的指令的关系,接下来一节再介绍。

因此当我们在理解代码时,如果想弄清楚当前线程里某些代码是否能看到公共变量在别的线程里被修改后的效果,我们应该第一时间这样来考虑,当前使用这些公共变量的代码能与别的线程里修改这些公共变量的代码建立一个 happen-before 的关系吗?如果有 happen-before 关系,则可以看到,如果没有则不一定,还是使用前面的例子:

// thread 0 g_payLoad.store(42, memory_order_relaxed); // 1 g_guard.store(1, memory_order_relaxed); // 2 // thread 1 int r = g_guard.load(memory_order_relaxed); // 3 if (r) r = g_payLoad.load(memory_order_relaxed); // 4

如果 g_payLoad.load() 要 load 到 #1 中所写入的值,则这两者之间必须要有 happen-before 关系,而他们之间要有 happen before 关系,只需满足如下两个条件之一:

它们真的有 happen-before 关系,即上述代码中, #1 happen-before #4, 不过这个没法只根据 1 和 4 这两行代码来证明, 原因下面一节会具体解释。

g_guard.store() 与 g_guard.load() 之间有 happen-before 关系, 即 #2 和 #3 之间有 happen-before 关系,则根据传递性,我们可以知道 #1 happen-before #4.

Synchronize-with

Sychronize-with 指的是两个线程间的指令在运行时的一种特殊关系,这种关系是通过 acquire/release 语义来达成的,具体来说,如果:

线程 A 中对某个变量 m 以 release 的方式进行修改,m.store(42, memory_order_release).

线程 B 中对变量 m 以 acquire 的方式进行读取,并且读到线程 A 所写的值(或以 A 为开始的一个 release sequence 所写入的值),则线程 B 中读 m 变量的那条指令与线程 A 中修改 m 的指令存在 synchronize-with 的关系。

顺便说一下 release sequence,操作 A 对变量 m 的 release sequence 指的是满足如下两个条件且作用于 m 上的操作的序列:

该操作与 A 在同一个线程内,且跟在 A 操作之后。

该操作在其它线程内,但是是以 Read-Modify-Write(RMW) 的方式对 m 进行修改。

其中, A 操作以 store release 的方式修改变量 m,而 RMW 操作是一种很特殊的操作,它的 read 部分要求永远能读到该变量最新的值。[参看 c++ 标准 29.3.12]

下图展示了一个经典的 synchonize-with 的例子:

C++11 内存模型详解(续)

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/a76ff1bb967428d5bcc70754a246c086.html