【问题标题】:How are memory_order_seq_cst fences useful anymore in C++20?在 C++20 中,memory_order_seq_cst 栅栏有什么用处?
【发布时间】:2021-10-29 18:53:51
【问题描述】:

考虑这段代码:

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;
}

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

我认为这在 C++17 中是正确的。这个想法是这样的:首先假设a得到0,然后证明b必须得到1。让我们把它分成两部分:

1。 Fence X 在 SC 总顺序中在 atomic op B 之前

  • X 在 P 之前排序,因此 X=>P
  • P 读取 0,Q 写入 1,因此 P 读取之前(从读取)Q,因此 P=>Q
    • 我在标准中没有找到保证这一点的条款,但this paper 是这么说的。如果我弄错了,请指出。 编辑:现在我知道了。这与 Nate 的回答中关于 coherence-ordered before 的规则相同。
  • Q 在 B 之前排序,因此 Q=>B

2。原子操作B读取原子操作A写入的值(1)

C++17 在标准中有这个:

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

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

以上推理正确吗?

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

对某个原子对象 M 的原子操作 A 在另一个原子操作 B 之前是连贯有序的 米如果

  • A是修改,B读取A存储的值,或者
  • 按照 M 的修改顺序,A 在 B 之前,或
  • A和B不是同一个原子读-修改-写操作,存在原子修改 M 中的 X 使得 A 读取 X 存储的值,并且 X 在 M 的修改顺序中位于 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 操作,那么在 S 中 A 在 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 操作或彼此一致排序的操作进行排序。

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

是我没有认真研究标准,还是代码在 C++20 中不再适用?如果是后者,是回归 故意的? (在 P0668 中,他们声称要“加强”围栏)。

【问题讨论】:

  • 是的,在 C++17 中 a == 1 || b == 1 是有保证的。宽松存储 + seq_cst 屏障至少与 seq_cst 存储一样强,并且源顺序的任何交错在另一个线程中的存储之后至少有一个负载。我还不了解 C++20,还没有阅读详细信息。我希望这仍然是委员会希望成为真实的东西,所以如果有可能是缺陷的变化。 (但我不希望现实世界的实现会改变他们编译它的方式,所以实际上在普通 CPU 上一切都很好,当然。)
  • 这只是关于 SC 栅栏相对于其他 SC 操作的排序方式。 - 不完全是。 "Second," 子句是关于同一对象 M 上的任何一对连贯有序的操作,例如包括碰巧看到来自 mo_relaxed 存储的值的 mo_relaxed 加载。 (eel.is/c++draft/atomics#order-3.1)。但我不确定它是否适用于 没有 看到存储值的负载。 3.2 如果 A 是读取而不是修改,我认为“A 在 M 的修改顺序中先于 B”将不适用?如果它确实适用,那么 SC 围栏项目符号列表也适用。
  • @PeterCordes 谢谢!我明白你的意思。如果您将 在写入 M(也称为“reads-before”)之前读取 M 的值算作 coherence-ordered before,您可以假设 a==0 &amp;&amp; b==0 然后说 B (x 的负载)在 A(x 的存储)之前是连贯有序的。然后应用第二个项目符号列表:“- 如果 A 是 memory_order::seq_cst 操作并且 B 发生在 memory_order::seq_cst 栅栏 Y 之前,则 A 在 S 中位于 Y 之前;”,你得到 B= >X in S。那是S中的一个圆(X=>P=>Q=>B=>X),这是被禁止的。所以a==0 &amp;&amp; b==0 不可能是真的。
  • 标准文本似乎没有说“reads-before”是“coherence-ordered before”。既然名字里有“连贯”这个词,也许应该是吧?
  • 我很确定,按原样读取旧值不会在存储之前进行负载连贯排序。 (但是,是的,如果确实如此,那将让 SC 围栏规则去做他们应该做的事情)。一个线程可能会看到对象 M 的存储,而另一个线程仍然看到旧值(例如 on real HW via store-forwarding between SMT threads),并且 IDK 标准的其他部分依赖于一致性排序的操作,所以 IDK 如果这可以修复标准不破坏其他任何东西。

标签: c++ language-lawyer c++20 memory-barriers memory-model


【解决方案1】:

是的,我想我们可以证明a == 1 || b == 1 总是正确的。这里的大部分想法都是 zwhconst 和 Peter Cordes 在 cmets 中完成的,所以我只是想把它写下来作为练习。

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

假设线程 2 中的 b = x.load() 产生 0。

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

(这里可能有一个差距:原子对象的初始化不是原子操作(3.18.1p3),因此措辞上,连贯性排序不适用于它。我不得不相信它打算在这里适用,不过。无论如何,我们可以通过在开始线程之前将x.store(0, std::memory_order_relaxed); 放入main 来避开这个问题,这仍然可以解决您问题的精神。)

现在在排序 S 的定义中,应用第二个项目符号,A = x.load() 和 B = x.store(1) 和之前一样,Y 是线程 1 中的 atomic_thread_fence。那么 A 在 B 之前是相干有序的,正如我们刚刚展示的那样; A是seq_cst;和 B 通过排序发生在 Y 之前。因此,A = x.load() 在 Y = fence 之前,顺序为 S。

现在假设线程 1 中的 a = y.load() 也产生 0。

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

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

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

【讨论】:

  • 啊,是的,我之前没有理解第三个要点。我想我看到 RMW 并停止寻找,错误地假设它会是什么。因此,如果看不到值,则加载 在商店之前是一致排序的。 (同意静态初始化算作 X 操作以匹配该措辞的意图)。是的,一旦我们可以证明称这种一致性排序是合理的,那么所有其他部分都会到位。
  • 不错!现在我知道“not the same”在英语中的含义:P
猜你喜欢
  • 2022-01-22
  • 1970-01-01
  • 1970-01-01
  • 2020-01-10
  • 1970-01-01
  • 2020-04-01
  • 2020-10-01
相关资源
最近更新 更多