C11原子与序列点的关系

Relationship between C11 atomics and sequence points

我基本上有以下代码片段:

size_t counter = atomic_fetch_sub_explicit(&atomicCounter, 1, memory_order_release);
if (counter - 1 == 0
    && atomic_load_explicit(&anotherAtomicCounter, 1, memory_order_relaxed) == 0 {
      //Some code
}

为了正确性,重要的是 anotherAtomicCounter 的原子加载发生在 atomicCounter 的获取和子 (FAS) 之后。对于给定的内存顺序,通常无法保证这一点,加载可能发生在 FAS 之前。但是,我想知道序列点如何影响这个特定代码。该标准提到

If evaluation A is sequenced before evaluation B, then evaluation of A will be complete before evaluation of B begins.

结合规则编号 2

There is a sequence point after evaluation of the first (left) operand and before evaluation of the second (right) operand of the following binary operators: && (logical AND), || (logical OR), and , (comma).

这意味着原子加载必须发生在比较之后,但只有在知道 FAS 的结果后才能完成比较。

我的问题是这些规则是否保证原子加载总是在 FAS 之后发生,即使在使用更宽松的内存顺序时也是如此?

提前致谢!

回答你标题中的问题,原子和序列点之间没有真正的关系。

编写的代码确实保证编译器必须在 atomic_load 之前执行 atomic_fetch_sub。但是这些函数(在 C 的内存模型中)只是请求平台在某些内存块上执行某些操作。当它们对谁可见时,它们的 效果 由内存模型和排序参数指定。因此,即使在您 知道 请求 A 在请求 B 之前出现的情况下,这并不意味着请求 A 的 effects 在请求 B 之前得到解决,除非你明确指定它是。

我会说一种关系,但它是间接的。

序列点是语言如何定义 sequenced before 评估的部分顺序 (C17 5.1.2.3),这基本上就是您通常所说的“程序顺序”。这是操作天真地执行的顺序,对于 single-threaded 程序,它完全决定了这些操作如何相互交互。例如,它承诺,如果您调用 foo() && bar()bar 将看到 foo 对全局变量所做的所有更新; foo() 的评估将在 bar() 的评估开始之前完成。

现在内存排序的全部要点是其他线程看到加载和存储的顺序不一定与程序顺序一致。如果你写

res = atomic_load_explicit(&a, memory_order_relaxed) || atomic_load_explicit(&b, memory_order_relaxed);

然后 a 的负载 b 的负载之前排序,这要归功于 || 提供的序列点。但是因为内存顺序是 relaxed,所以不能保证其他线程以相同的顺序看到这些操作。如果我们之前有 a == 0b == 1,而另一个线程执行 atomic_store(&a, 1); atomic_store(&b, 0);(假设使用 seq_cst 顺序),那么 res 完全有可能结束值为 0.

这与您的示例代码的情况相同。您在 fetch_subload 之间有一个序列点。 (实际上有两个:一个在初始化 counter 的完整表达式的末尾,另一个在您指出的 && 处。)所以 fetch_sub 绝对是 load 之前排序。但是您的内存排序不够强大,无法保证 inter-thread 可见性与排序相匹配。

然而,内存屏障可以给你更多的确定性,它们所做的是确保在某种特定范围内,外部观察者的可见顺序确实与程序一致命令。如果你写

res = atomic_load_explicit(&a, memory_order_seq_cst) || atomic_load_explicit(&b, memory_order_seq_cst);

然后序列点再次保证 a 的加载在 b 的加载之前排序,现在 seq_cst 保证其他线程按该顺序看到这些操作也。现在 res 如果另一个线程 atomic_store(&a, 1); atomic_store(&b, 0);.

将始终设置为 1

但是内存障碍最多只能强加与已经提供的排序一样多的排序。如果你改为

res = atomic_load_explicit(&a, memory_order_seq_cst) + atomic_load_explicit(&b, memory_order_seq_cst);

现在障碍对你没有帮助。两个负载之间没有顺序点,因此无法保证它们被评估的顺序,更不用说它们变得可见的顺序了。

形式上,sequenced before关系用于定义5.1.2.4的happens before关系,这是主要的工具确定来自不同线程的加载和存储如何相互影响,特别是确定它们何时导致和不导致数据竞争。