在同一个原子变量上混合使用 relaxed 和 acquire/release 访问如何影响同步?

How does mixing relaxed and acquire/release accesses on the same atomic variable affect synchronises-with?

我对 C++ 内存模型中同步关系的定义有疑问,当松弛和 acquire/release 访问混合在同一个原子变量上时。考虑以下由全局初始化程序和三个线程组成的示例:

int x = 0;
std::atomic<int> atm(0);

[thread T1]
x = 42;
atm.store(1, std::memory_order_release);

[thread T2]
if (atm.load(std::memory_order_relaxed) == 1)
    atm.store(2, std::memory_order_relaxed);

[thread T3]
int value = atm.load(std::memory_order_acquire);
assert(value != 1 || x == 42);  // Hopefully this is guaranteed to hold.
assert(value != 2 || x == 42);  // Does this assert hold necessarily??

我的问题是 T3 中的第二个断言在 C++ 内存模型下是否会失败。请注意, 表示如果 T2 使用 load/acquire 和 store/release,则断言不会失败;如果我弄错了,请纠正我。然而,如上所述,答案似乎取决于在这种情况下如何准确定义同步关系。我被cppreference上的文字弄糊涂了,想出了以下两种可能的解读。

  1. 第二个断言失败。 atm in T1 的存储在概念上可以理解为存储 1_release 其中 _release 是指定值存储方式的注解;同样,T2 中的存储可以理解为存储 2_relaxed。因此,如果在 T3 returns 2 中加载,线程实际读取 2_relaxed;因此,T3 中的负载不会T1 中的存储同步,并且不能保证 T3 看到 x == 42 .但是,如果 T3 returns 1 中的加载,则 1_release 被读取,因此 T3 中的加载与 [=15] 中的存储同步=] 和 T3 保证看到 x == 42.

  2. 第二次断言成功。如果在 T3 returns 2 中加载,则此加载在 T2 中读取 relaxed store 的副作用;但是,仅当 atm 的修改顺序包含具有释放语义的先前存储时,T2 的此存储才会出现在 atm 的修改顺序中。因此,T3中的load/acquire与T1中的store/release是同步的,因为在atm.[=56的修改顺序中,后者必然在前者之前=]

乍一看,the answer to this SO question似乎暗示我的读法1是正确的。然而,这个答案似乎有微妙的不同:答案中的所有商店都是发布的,问题的症结在于看到 load/acquire 和 store/release 在一对之间建立同步线程。相比之下,我的问题是当内存顺序是异构的时候,synchronises-with 是如何定义的。

我真的希望阅读 2 是正确的,因为这会使并发推理更容易。线程T2不读写除atm以外的任何内存;因此,T2 本身没有同步要求,因此应该能够使用宽松的内存顺序。相比之下,T1 发布 x 并且 T3 消费它——也就是说,这两个线程相互通信,因此它们显然应该使用 acquire/release 语义。换句话说,如果解释 1 被证明是正确的,那么代码 T2 不能只考虑 T2 做了什么就可以写出来;相反,T2 的代码需要知道它不应该“干扰”T1T3 之间的同步。

无论如何,了解在这种情况下标准究竟批准了什么对我来说绝对是至关重要的。

因为您在 T2 中的单独加载和存储上使用宽松排序,释放序列被破坏并且第二个断言可以触发(尽管不是在 X86 等 TSO 平台上)。
您可以通过在线程 T2 中使用 acq/rel 排序(如您建议的那样)或修改 T2 以使用原子 read-modify-write 操作(RMW)来解决此问题,如下所示:

[Thread T2]
int ret;
do {
    int val = 1;
    ret = atm.compare_exchange_weak(val, 2, std::memory_order_relaxed);
} while (ret != 0);

atm 的修改顺序是 0-1-2,T3 将在 1 或 2 上拾取并且断言不会失败。

T2 的另一个有效实现是:

[thread T2]
if (atm.load(std::memory_order_relaxed) == 1)
{
    atm.exchange(2, std::memory_order_relaxed);
}

这里RMW本身是无条件的,必须伴随着一个if-statement & (relaxed) load来保证atm的修改顺序是0-1或者0-1-2
如果没有 if-statement,修改顺序可能是 0-2,这会导致断言失败。 (这是有效的,因为我们知道在整个程序的其余部分中只有一个其他写入。单独的 if() / exchange 当然 而不是 通常等同于compare_exchange_strong.)

在 C++ 标准中,以下引用是相关的:

[intro.races]
A release sequence headed by a release operation A on an atomic object M is a maximal contiguous subsequence of side effects in the modification order of M, where the first operation is A, and every subsequent operation is an atomic read-modify-write operation.

[atomics.order]
An atomic operation A that performs a release operation on an atomic object M synchronizes with an atomic operation B that performs an acquire operation on M and takes its value from any side effect in the release sequence headed by A.

this question 是关于 为什么 RMW 在发布序列中工作。