在没有 seq-cst 负载的情况下,seq-cst 围栏是否与 acq-rel 围栏完全相同?

Are seq-cst fences exactly the same as acq-rel fences in absence of seq-cst loads?

我正在尝试了解 std::atomic_thread_fence(std::memory_order_seq_cst); 围栏的用途,以及它们与 acq_rel 围栏的不同之处。

到目前为止我的理解是,唯一的区别是 seq-cst 栅栏影响 seq-cst 操作的全局顺序 ([atomics.order]/4)。并且只有在您实际执行 seq-cst 加载时才能遵守上述顺序。

所以我想如果我没有 seq-cst 负载,那么我可以用 acq-rel 栅栏替换我所有的 seq-cst 栅栏而不改变行为。对吗?

如果这是正确的,为什么我会看到像 this "implementation Dekker's algorithm with Fences" 这样的代码,它使用 seq-cst 栅栏,同时保持所有原子 reads/writes 放松?这是该博客 post:

中的代码
std::atomic<bool> flag0(false),flag1(false);
std::atomic<int> turn(0);

void p0()
{
    flag0.store(true,std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_seq_cst);

    while (flag1.load(std::memory_order_relaxed))
    {
        if (turn.load(std::memory_order_relaxed) != 0)
        {
            flag0.store(false,std::memory_order_relaxed);
            while (turn.load(std::memory_order_relaxed) != 0)
            {
            }
            flag0.store(true,std::memory_order_relaxed);
            std::atomic_thread_fence(std::memory_order_seq_cst);
        }
    }
    std::atomic_thread_fence(std::memory_order_acquire);
 
    // critical section


    turn.store(1,std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_release);
    flag0.store(false,std::memory_order_relaxed);
}

void p1()
{
    flag1.store(true,std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_seq_cst);

    while (flag0.load(std::memory_order_relaxed))
    {
        if (turn.load(std::memory_order_relaxed) != 1)
        {
            flag1.store(false,std::memory_order_relaxed);
            while (turn.load(std::memory_order_relaxed) != 1)
            {
            }
            flag1.store(true,std::memory_order_relaxed);
            std::atomic_thread_fence(std::memory_order_seq_cst);
        }
    }
    std::atomic_thread_fence(std::memory_order_acquire);
 
    // critical section


    turn.store(0,std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_release);
    flag1.store(false,std::memory_order_relaxed);
}

据我了解,它们是不一样的,反例是 以下。我认为您的逻辑错误在于:

And said order can only be observed if you actually perform seq-cst loads.

我不认为那是真的。在 atomics.order p4 中定义了 顺序一致性全序 S 的公理,第 2-4 项都可能 涉及不是 seq_cst 的操作。你可以观察 这些操作之间的一致性排序,这可以让你推断 seq_cst 操作是如何排序的。

例如,考虑以下版本的 StoreLoad 石蕊测试,类似于 Peterson 的算法:

std::atomic<bool> a,b;  // initialized to false

void thr1() {
    a.store(true, std::memory_order_seq_cst);
    std::atomic_thread_fence(std::memory_order_seq_cst);
    if (b.load(std::memory_order_relaxed) == false)
        std::cout << "thr1 wins";
}

void thr2() {
    b.store(true, std::memory_order_seq_cst);
    std::atomic_thread_fence(std::memory_order_seq_cst);
    if (a.load(std::memory_order_relaxed) == false)
        std::cout << "thr2 wins";
}

注意所有负载都是 relaxed

我声称如果 thr1 打印“thr1 wins”,那么我们推断 a.store(true) 在顺序一致性中先于 b.store(true) 订单S.

要看到这一点,设 A 为 b.load(),B 为 b.store(true)。如果 b.load() == false 然后我们有 A 之前是相干有序的 B.(将 atomics.order p3.3 与上面的 A、B 和 X 一起应用 b 初始化为 false。)

现在让 X 成为 thr1 中的围栏。然后 X 发生在 A 之前 排序,因此 X 在 S 中的 B 之前 atomics.order p4.3。那就是 thr1 围栏在 b.store(true) 之前。 a.store(true),也就是 也 seq_cst,在 thr1 栅栏之前,因为存储强烈 通过排序发生在栅栏之前。所以通过 传递性,a.store(true)b.store(true) 之前,如所声称的那样。

同样,如果thr2打印,那么b.store(true)先于 a.store(true)。他们不能都先于对方,所以我们有 因此证明该程序无法打印这两条消息。

如果将栅栏降级为 acq_rel,证明就会失效。在 在这种情况下,据我所知,没有什么能阻止程序 打印 thr1 wins 即使 b.store(true)a.store(true) 之前 按照 S 的顺序。因此,有 acq_rel 个栅栏,我相信它是 允许两个线程打印。虽然我不确定是否有 是它可能实际发生的任何真实实现。


如果我们将所有的加载和存储都设为 relaxed,那么我们可以得到一个更简单的示例,这样唯一的 seq_cst 操作就是栅栏。那么我们可以用 (4.4) 代替来证明如果 b.load(relaxed) returns false,thr1 栅栏在 thr2 栅栏之前,反之亦然,如果 a.load() returns false。因此,我们得出结论,和以前一样,该程序无法打印这两条消息。

但是,如果我们保持装载和存储放松,并将围栏削弱到 acq_rel,那么很明显我们已经失去了这种保证。事实上,稍加刺激,一个类似的例子实际上在 x86 上失败了,其中 acq_rel 栅栏是空操作,因为 loads/stores 已经是 acquire/release。所以这是一个更清晰的案例,其中 seq_cst 栅栏确实实现了 acq_rel 没有的东西。

总结 Nate Eldredge 在他们的回答中提出的论点:


全局 seq-cst 顺序本身似乎不会影响除 seq-cst 加载以外的任何内容,但即使没有加载,该顺序也必须存在。

某些操作重新排序(例如,在 Nate 的示例中,两个线程都进入临界区)会对 seq-cst 顺序施加矛盾的约束,因此是不允许的。

我不完全确定如果没有 seq-cst 栅栏(即仅使用 seq-cst 存储)是否可以观察到这种效果,我认为不会。

在 C++20 之前,C++ 标准包含以下段落以根据单个全阶 S(参见 [atomics.order])定义可见性:

For an atomic operation B that reads the value of an atomic object M, if there is a memory_order_seq_cst fence X sequenced before B, then B observes either the last memory_order_seq_cst modification of M preceding X in the total order S or a later modification of M in its modification order.

For atomic operations A and B on an atomic object M, where A modifies M and B takes its value, if there is a memory_order_seq_cst fence X such that A is sequenced before X and B follows X in S, then B observes either the effects of A or a later modification of M in its modification order.

For atomic operations A and B on an atomic object M, where A modifies M and B takes its value, if there are memory_order_seq_cst fences X and Y such that A is sequenced before X, Y is sequenced before B, and X precedes Y in S, then B observes either the effects of A or a later modification of M in its modification order.

然而,WG21/P0668R5 提出了两个被 C++20 接受的变化:首先,它削弱了单一全序的一些保证(它不再需要与“先发生”保持一致) ,其次(对于这个问题更重要的是),它 加强了 seq-cst 栅栏提供的保证。为此,他们用您问题中引用的措辞替换了第 32.4 节 [atomics.order] 的第 3-7 段(SC 排序的定义)。 P0668R5 状态:

This new wording takes a significantly different approach with respect to the sequential consistency ordering S: Instead of defining visibility in terms of S and the other orders in the standard, this essentially defines constraints on S in terms of visibility in a particular execution, as expressed by the coherence order. If these constraints are not satisfiable by any total order S, then the candidate execution which gave rise to the coherence order is not valid.

老实说,我发现这个新定义不太清楚,但我想我只需要再通读几遍,并试着理解它。 :) 但是,由于这应该 加强 保证,因此先前标准中描述的可见性保证仍然有效。

Seq-cst 栅栏也是单个全序 S 的一部分,因此任何两个 seq-cst 栅栏总是完全有序的。假设我们有一个写操作 A 在 seq-cst fence X 之前排序,还有一个读操作 B 在 seq-cst fence Y 之后排序。如果 XS 中排在 Y 之前,那么 B 保证观察到 A 写入的值,或一些后来的值(这是基于第一引述的最后一段)。 acq-rel fence 不提供任何此类保证。

Acq-rel 栅栏基本上将周围的松弛 load/stores 转换为 acquire/release 操作,但 acq-rel 仅在获取负载与发布存储同步时为周围操作提供可见性保证。也就是说,只有在负载 已经观察到 存储的值时。但它不提供任何保证何时商店可见。