memory_order_seq_cst 栅栏在 C++20 中有何用处?

zwh*_*nst 7 c++ memory-model memory-barriers language-lawyer c++20

考虑这段代码:

std::atomic<int> x{ 0 };
std::atomic<int> y{ 0 };
int a;
int b;

void thread1()
{
    //atomic op A
    x.store(1, std::memory_order_relaxed);

    //fence X
    std::atomic_thread_fence(std::memory_order_seq_cst);
    //sequenced-before P, thus in SC order X=>P

    //atomic op P
    a = y.load(std::memory_order_seq_cst);//0
    //reads-before(from-read) Q, thus in SC order P=>Q
}

void thread2()
{
    //atomic op Q
    y.store(1, std::memory_order_seq_cst);
    //sequenced-before B, thus in SC order Q=>B

    //atomic op B
    b = x.load(std::memory_order_seq_cst);
}

int main()
{
    std::thread t2(thread2);
    std::thread t1(thread1);
    t1.join();
    t2.join();
    assert(a == 1 || b == 1);//true?
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

问题是:a == 1 || b == 1在 C++20 中断言总是正确吗?

我认为在 C++17 中确实如此。思路是这样的:首先假设a得到0,然后证明b一定得到1。我们把它分成两部分:

1. 在 SC 总顺序中,栅栏 X 位于原子操作 B 之前

  • X 排序在 P 之前,因此 X=>P
  • P 读 0,Q 写 1,因此 P 读在(从读)Q 之前,因此 P=>Q
    • 我在标准中没有找到保证这一点的条款,但这篇论文是这么说的。如果我说错了请指出。编辑:现在我知道了。正如 Nate 的回答所示,这与before 的连贯性排序规则相同。
  • Q 排序在 B 之前,因此 Q=>B

2. 原子操作 B 读取原子操作 A 写入的 value(1)

C++17 标准中有这样的内容:

对于原子对象 M 上的原子操作 A 和 B,其中 A 修改 M 并且 B 获取其值,如果存在 memory_order::seq_cst 栅栏 X,使得 A 在 S 中排序在 X 之前,B 在 X 之后,则 B 观察到A 或 M 在其修改顺序中的后续修改的效果。

这里的情况正是如此。Op A 和 B 位于原子对象 x 上,其中 A 在 x 中存储 1,B 读取 x 的值;还有 seq_cst 栅栏 X;S 中 A 排在 X 之前,B 排在 X 之后(上);并且 M 晚于 A 没有修改。因此 B 观察 A 写入的值。因此 b 得到 1。

上述推理正确吗?

但在 C++20 中,上面有关栅栏的文本被删除(由P0668),并升级为“强化”的 seq_cst 栅栏,内容如下:

某个原子对象 M 上的原子操作 A 在 M 上的另一个原子操作 B 之前是一致有序的,如果

  • A是修改,B读取A存储的值,或者
  • 在 M 的修改顺序中,A 位于 B 之前,或者
  • A 和 B 不是同一个原子读-修改-写操作,并且存在 M 的原子修改 X,使得 A 读取 X 存储的值,并且按照 M 的修改顺序,X 在 B 之前,或者
  • 存在 M 的原子修改 X,使得 A 在 X 之前是相干有序的,并且 X 在 B 之前是相干有序的。

所有 memory_order::seq_cst 操作(包括栅栏)上都有一个总顺序 S,满足以下约束。首先,如果 A 和 B 是 memory_order::seq_cst 操作,并且 A 强烈发生在 B 之前,则 A 在 S 中先于 B。其次,对于对象 M 上的每对原子操作 A 和 B,其中 A 在 B 之前是一致有序的,S需要满足以下四个条件:

  • 如果A和B都是memory_order::seq_cst操作,那么A在S中先于B;和
  • 如果 A 是 memory_order::seq_cst 操作并且 B 发生在 memory_order::seq_cst 栅栏 Y 之前,则 A 在 S 中先于 Y;和
  • 如果 memory_order::seq_cst 栅栏 X 发生在 A 之前,并且 B 是 memory_order::seq_cst 操作,则 X 在 S 中先于 B;和
  • 如果 memory_order::seq_cst 栅栏 X 发生在 A 之前,且 B 发生在 memory_order::seq_cst 栅栏 Y 之前,则 X 在 S 中先于 Y。

我不明白如何a==1 || b==1保证了。这仅涉及 SC 栅栏如何相对于其他 SC 操作或彼此一致排序的操作进行排序。

我在 C++20 标准中找不到任何有关 SC 栅栏如何与非 SC 原子操作交互的信息,并且看不到存储中的值的加载并不意味着它在存储之前是一致排序的。(或者是吗?如果是这样,就可以解决问题;请参阅注释)31.4 : 3.2适用于M 的修改顺序中 A 先于 B 的情况,但读取不是对象修改顺序的一部分,不是吗?

是我没有足够努力地研究标准,还是代码根本无法在 C++20 中工作?如果是后者,回归是故意的吗?(在P0668中,他们声称要“加固”围栏)。

Nat*_*dge 3

是的,我认为我们可以证明这a == 1 || b == 1始终是正确的。这里的大部分想法都是 zwhconst 和 Peter Cordes 在评论中提出的,所以我只是想把它写下来作为练习。

(请注意,下面的 X、Y、A、B 用作标准公理中的虚拟变量,并且可能逐行更改。它们与代码中的标签不一致。)

假设b = x.load()在 thread2 中产生 0。

我们确实有您询问的连贯性顺序。具体来说,如果b = x.load产生 0,那么我声称x.load()在 thread2 中的一致性排序先于在 thread1 中排序x.store(1),这要归功于一致性排序定义中的第三个项目符号。设 A 为x.load()、B 为x.store(1)、X 为初始化x{0}(请参阅下面的争论)。显然,在 的修改顺序中,X 优先于 B x,因为 X 发生在 B 之前(同步发生在线程启动时),并且如果b == 0A 已读取 X 存储的值。

(这里可能存在差距:原子对象的初始化不是原子操作(3.18.1p3),因此按照措辞,连贯顺序不适用于它。不过,我必须相信它旨在适用于此。不管怎样,我们可以通过在启动线程之前x.store(0, std::memory_order_relaxed);插入来回避这个问题main,这仍然可以解决您问题的实质。)

现在,在顺序 S 的定义中,像以前一样应用第二个项目符号 A =x.load()和 B = x.store(1),Y 是atomic_thread_fencethread1 中的。那么 A 在 B 之前是一致有序的,正如我们刚才所展示的;A 是seq_cst; 通过排序,B 发生在 Y 之前。因此,A =按 S 顺序x.load()先于 Y =。fence

现在假设a = y.load()thread1 也产生 0。

通过与 before 类似的论点,y.load()在 之前 是连贯有序的y.store(1),并且它们都是seq_cst,因此在 S 中先于。此外,通过排序在 S 中y.load()先于,同样 在 S 中先于。因此,我们在 S 中有:y.store(1)y.store(1)x.load()atomic_thread_fencey.load()

  • x.load先于fence先于y.load先于y.store先于x.load

这是一个循环,与 S 的严格排序相矛盾。