首页 > 解决方案 > 释放序列的概念在实践中有用吗?

问题描述

C++ 原子语义仅保证执行释放写入(简单或读取-修改-写入)操作的最后一个线程执行的内存操作的可见性(通过先发生关系)。

考虑

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++ 概念的先发生或释放序列;我看不出这些概念映射到什么真实的硬件细节。

a当 2 in 的值是全局可见时,真正的实现怎么能不保证线程 1 内存操作的可见性?

换句话说,发布序列定义有什么好处吗?为什么发布序列不会扩展到修改顺序中的每个后续修改?

特别考虑愚蠢的线程3:

if (a.load(memory_order_relaxed) == 2))
  a.store(2,memory_order_relaxed);

silly-thread 3 能否抑制任何真实硬件上的任何可见性保证?换句话说,如果值 2 是全局可见的,如何使它再次全局可见会破坏任何排序?

我的真实多处理心智模型不正确吗?可以在某些 CPU 上部分可见的值但注意另一个值吗?

(当然,我假设宽松写入的非疯狂语义,因为回溯到时间的写入使 C++ 的语言语义绝对荒谬,不像像 Java 这样总是具有有限语义的安全语言。没有真正的实现可以有疯狂的、非因果的宽松语义。)

标签: c++multithreadingthread-safetymemory-modelstdatomic

解决方案


我们先回答你的问题:

为什么发布序列不会扩展到修改顺序中的每个后续修改?

因为如果是这样,我们将失去一些潜在的优化。例如,考虑线程:

x = 1;                            // #1
a.store(1,memory_order_relaxed);  // #2

在当前规则下,编译器能够重新排序#1 和#2。但是,在 release-sequence 扩展之后,编译器不允许对这两行重新排序,因为像你的线程 2 这样的另一个线程可能会引入一个以 #2 为首,以释放操作为尾的释放序列,因此可能有些读- 另一个线程中的获取操作将与#2 同步。


你举了一个具体的例子,并声称所有的实现都会产生一个特定的结果,而语言规则并不能保证这个结果。这不是问题,因为语言规则旨在处理所有情况,而不仅仅是您的具体示例。当然,可以改进语言规则,以便它可以保证您的具体示例的预期结果,但这不是一项简单的工作。至少,正如我们上面所讨论的,简单地扩展释放序列的定义并不是一个公认的解决方案。


推荐阅读