C / C ++原子语义仅保证通过执行释放“简单”写(无读-修改-写)操作的最后一个线程执行的内存操作(通过事前发生关系)。 (在写操作和RMW操作之间存在强烈的语义区分。)
考虑
int x, y;
atomic<int> a;
线程1:
x = 1;
a.store(1,memory_order_release);
线程2:
y = 2;
if (a.load(memory_order_relaxed) == 1))
a.store(2,memory_order_release);
然后观察a == 2
意味着线程2操作(y == 2
)的可见性,而不是线程1的可见性(一个人甚至无法读取x
)。
据我所知,多线程的实际实现使用围栏(有时是发布存储)的概念,但没有发生在高级C / C ++概念上的事前或发布顺序;我看不到这些概念映射到哪些真正的硬件细节。
a
中的值2全局可见时,真正的实现如何不能保证线程1内存操作的可见性?
换句话说,发布顺序定义有什么好处吗?为什么释放顺序不按修改顺序扩展到每个后续修改?
请特别考虑傻线程3:
if (a.load(memory_order_relaxed) == 2))
a.store(2,memory_order_relaxed);
傻线程3可以抑制任何真实硬件上的可见性保证吗?换句话说,如果值2在全局范围内可见,那么如何使其再次在全局范围内可见会破坏任何顺序?
我真正的多处理思维模型是否正确?在某些CPU上可以部分显示一个值,但注意另一个吗?
(当然,我假设轻松写的是非疯狂的语义,因为时光倒流的写使得C / C ++的语言语义绝对是毫无意义的,不像Java这样的安全语言总是具有受限的语义。没有真正的实现可以使疯狂,非因果的宽松语义。)
答案 0 :(得分:1)
让我们首先回答您的问题:
为什么发行顺序不按修改顺序扩展到每个后续修改?
因为这样,我们将失去一些潜在的优化。例如,考虑以下线程:
x = 1; // #1
a.store(1,memory_order_relaxed); // #2
在当前规则下,编译器可以对#1和#2重新排序。但是,在释放序列的扩展之后,不允许编译器对这两行进行重新排序,因为另一个线程(例如线程2)可能会引入以#2开头并以释放操作为尾的释放序列,因此有可能进行一些读取-另一个线程中的-acquire操作将与#2同步。
您举了一个具体的示例,并声称所有实现将产生特定的结果,而语言规则并不保证该结果。这不是问题,因为语言规则旨在处理所有情况,而不仅仅是您的特定示例。当然,可以改进语言规则,以保证特定示例的预期结果,但这不是一件容易的事。至少如上所述,仅仅扩展发布序列的定义是不可接受的解决方案。