这里有一点需要明确,操作 A 与 操作 B 是否存在 synchronize-with 关系,关键在于 B 是否读到了 A 所写入的内容(或 release sequence 写入的内容), 而 B 是否能读到 A 写入的内容与 A 这个操作是否已经进行了无关,c++ 标准的规定是,假如读到了,则 A synchronize-with B, 否则则不,因此对于某些关键变量,如果你想保证当你去读它时,总能读到它在别的线程里写入的最新的值,一般来说,你需要额外再设置一个 flag 用于进行同步,一个标准的模式就是前面例子中的 g_payLoad 与 g_guard。 g_payLoad 是你关注的关键信息或者说想要发布到别的线程的关键信息,而 g_guard 则是一个 flag,用于进行同步或者说建立 happen-before 的关系,只有建立了 happen-before 关系,你去读 g_payLoad 时,才能保证读到最新的内容。
sequential consistencysequential consistency 这种模型实在是太美好了,它让编码变得这样地简单直接,一切都是和谐有序的,社会主义般地美好,而这种美好又是那么地触手可及,只要你完全不要使用其它模型,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'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); // thread 1 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