再说 c++11 内存模型(二)
型,SC 就是你的了!而你所需付出的代价只是一点点效率上损失,就那么一点点!但不幸 c++ 程序员里面处女座的太多,因此我们得处理 acquire/release,甚至是 relaxed。而当 SC 与其它模型混合在了一起时,一定不要想当然以为有 SC 出现的地方就都是曾经的美好乐园,不一定了。
?
以 sequential consistency 方式进行的 load() 操作含有 acquire 语义。
以 sequential consistency 方式进行的 store() 操作含有 release 语义。
所有 sequential consistency 操作在全局范围内有一个一致的顺序,但这个顺序与 happen-before/synchronize-with 没有直接联系,sequential consistency Load 不一定会 Load() 到最新的值,sequential consistency write() 也并不一定就马上会被其它非 sequential consistency load() 所能 load() 到。
需要注意的是 sequential consistency 类型的 fence,它是个例外,和纯粹的 SC load 和 SC store 不同,SC fence 能建立 happen-before 关系,参看 c++ 标准 29.3.6:
?
假如存在两个作用于变量 m 的操作 A 和操作 B,A 修改 m,而 B 读取 m,如果存在两个 memory_order_seq_cst 类型的 fence X 与 Y,使得:
1. A sequence-before X,且 Y sequence-before B.
2. 且 X 在全局顺序上处于 Y 之前(因为 X 和 Y 是 memory_order_seq_cst 类型的,因此肯定有一个全局顺序)。
则 B 会读到 A 写入的数据。
?
Dekker and Petterson's Algo
?
现在让我们尝试用前面介绍的知识来解决两个问题,如下是一个简化版的 Dekker's Algo,假设所有数据的初始值都是 0,则显然,如果所有内存操作都是以 relaxed 方式进行的话,则 r1 == r2 == 0 是可能的,因为 thread 0 里对 g_a 的读取不一定能看到 thread 1 对 g_a 的修改,对 g_b 的读取同理,现在的问题是,怎么才能阻止同时读到 r1 == r2 == 0?
?
// thread 0
g_a.store(42, memory_order_relaxed); // 1
r1 = g_b.load(memory_order_relaxed); // 2
?
// thread 1
g_b.store(24, memory_order_relaxed); // 3
r2 = g_a.load(memory_order_relaxed); // 4.
直接机械地套用 acquire/release 是不行的,#1 和 #4 不一定能建立 synchronize-with 关系,且 g_a 本身是关键变量,我们需要保证的是能读到它的最新值,直接用它来建立 synchonize-with 显然不能保证这点,#3 和 #2 同理。一个解法是分别在两个 thread 里分别加入一个 SC fence:
?
// thread 0
g_a.store(42, memory_order_relaxed);
atomic_thread_fence(memory_order_seq_cst); // fence 1
r1 = g_b.load(memory_order_relaxed);
?
g_b.store(24, memory_order_relaxed);
atomic_thread_fence(memory_order_seq_cst); // fence 2
r2 = g_a.load(memory_order_relaxed); // 4
原理很简单,因为 fence 1 和 fence 2 是 sequential consistency 类型的, 因此它们的副作用在全局上有一个固定顺序,要么 fence 1 先于 fence 2,要么 fence 2 先于 fence 1,根据前一节的介绍,我们知道要么 g_a 读到 42, 要么 g_b 读到 24, 因此肯定不会出现 r1 == r2 == 0.
?
现在是第二个问题,如下是 Peterson's Algo 的简化写法, 用于实现互斥锁,问题的关键是怎么保证 flag0 在线程 1 里能读到线程 0 对它的修改?等价问题是怎么阻止 #3 被 reorder 到 #1 之前,#6 被 reorder 到 #4 之前?
?
// Thread 0
flag0.store(true, memory_order_relaxed); // 1
r0 = turn.store(0, memory_order_relaxed); // 2
r1 = flag1.load(memory_order_relaxed); // 3
?
// Thread 1
flag1.store(true, memory_order_relaxed); // 4
r0 = turn.exchange(1, memory_order_relaxed); // 5
r1 = flag0.load(memory_order_relaxed); // 6
现在我们尝试用 acquire/release 语义来解决它,假设 thread 0 先执行并进入了临界区,然后 thread 1 后执行,当 thread 1 执行到 #6 时,怎么保证能看到 thread 0 对 flag0 的修改呢?根据前面第二节的介绍,我们知道关键在于要保证 #1 happen-before #6,又由于 #1 和 #6 分别在不同的线程,因此其实就是要保证 #1 synchronize-with #6,因此我们需要在 thread 0 中以 release 的方式写一个变量 A,然后在 thread 1 中以 acquire 的方式读取该变量 A,那么我们应该选取哪个变量作为这个关键变量呢?
?
flag0 不行,原因与前面第一节的例子相同,flag0 是我们要读取的关键变量,我们要保证的是能读取到它的最新值,而不是通过它来实现 synchronize-with.
flag1 也不行,flag1 在 thread 0 只有一个 load 操作,没有 release 语义(但如果用 flag1.fetech_add(0, memory_order_acq_rel) 呢?应该也是行的,只是不是最好,多了一次无谓的写操作)。
最优选择应该是 turn 变量。
因此得到如下解法如下:
?
// Thread 0
flag0.store(true, memory_order_relaxed); // 1
r0 = turn.exchange(1, memory_order_acq_rel); // 2
r1 = flag1.load(memory_order_acquire); // 3
?
// Thread 1
flag1.store(true, memory_order_relaxed); // 4
r0 = turn.exchange(2, memory_order_acq_rel); // 5
r1 = flag0.load(memory_order_acquire);