【发布时间】: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 && 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 && 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