【问题标题】:memory_order_relaxed and visibilitymemory_order_relaxed 和可见性
【发布时间】:2021-05-09 06:53:24
【问题描述】:

考虑两个线程,T1 和 T2,它们分别存储和加载原子整数 a_i。让我们进一步假设在加载开始执行之前执行存储。之前,我的意思是绝对意义上的时间。

T1                                    T2
// other_instructions here...         // ...
a_i.store(7, memory_order_relaxed)    // other instructions here
// other instructions here            // ...
                                      a_i.load(memory_order_relaxed)
                                      // other instructions here

是否保证T2在加载后看到值7?

【问题讨论】:

  • 线程“同步”是什么意思?是条件变量还是用于对操作进行排序的东西?
  • 那你怎么知道哪个先发生?
  • “在执行时间线中一个接一个” - “执行时间线”是什么意思?在多线程方面没有通用的时间表。
  • 在 C++ 中(在我们的物理宇宙中也没有)没有所谓的“绝对时间感”。 C++ 标准没有根据绝对时间来定义任何东西。
  • T2 保证可以看到曾经存储在 a_i 中的值之一,包括 7。T2 永远不会看到未存储的值(如果 a_i 不是 @ 987654324@)。但是,如果周围的代码不保证a_i 上的操作顺序,则无法保证它会看到哪个值。这种保证必须使用C++表达式之间的happens-before(线程内)和synchronizes-with(线程间)关系来建立,后者是通过获取和释放操作来实现的。所以你仍然需要在你的代码中somewhere获取/释放操作。

标签: c++ atomic cpu-architecture stdatomic


【解决方案1】:

是否保证T2在加载后看到值7?

这里的内存顺序无关紧要;原子操作是原子的。只要您确保写入“发生在”读取之前(您在问题的前提下声明为真),并且没有其他干预操作,T2 将读取 T1 写入的值。这是原子操作的本质,内存顺序不会改变这一点。

控制什么内存顺序如果T2看到7(是否保证“happens-before”),是否可以访问T1修改的other数据之前它将 7 存储到原子中。而使用relaxed 内存排序,T2 没有这样的保证。


注意:您将问题从关于 load "happens after" the store 的情况(当商店为 explicitly "synchronized" with the load)更改为更加模糊的情况。就 C++ 对象模型而言,没有“绝对时间”。对特定原子对象的所有原子操作都按顺序发生,但除非有某些东西显式在两个加载之间创建了“发生之前/之后”的关系,否则无法知道加载了什么值。这将是两种可能性之一,但不知道是哪一种。

【讨论】:

  • 这是不正确的。OP将“之前”定义为“在绝对时间意义上”。这并不能保证商店是在加载之前订购的。根据定义,这 2 个操作是有序的,但您只能通过评估加载的结果来确定顺序。如果加载发生(比方说)在存储(时钟时间)之后不到一微秒,由于存储缓冲区的影响,它可以(并且可能会)返回旧值。
  • @LWimsey:问题at the time I composed my answer 表示“发生在之后”,这是一个定义明确的 C++ 术语。早期版本甚至使用了“同步”一词。此后,它变得更加模糊。
【解决方案2】:

(我正在回答 更新 问题;Nicol 回答了在 C++“happens-before”术语中指定“after”的原始问题,包括同步,这意味着保证读者可以看到作者所做的事情。并不是说它们以循环的锁步循环运行;C++ 没有任何“循环”的概念。)

我正在回答 C++ 如何在普通的现代 CPU 上运行。 ISO C++ 当然对 CPU 架构只字未提,只是在关于 C++ 标准中atomic<> 一致性保证的目的的注释中提到普通硬件具有一致的缓存。

之前,我的意思是绝对意义上的时间。

如果您的意思是存储在加载执行之前全局可见,那么根据定义,是的,加载会看到它。但是,如果您指的是正常计算机架构意义上的“执行”,那么 不,没有保证。如果它们同时在不同的内核上运行,则存储需要一些时间才能对其他线程可见。

现代 CPU use a store buffer to decouple store execution from visibility to other cores,因此执行可以是推测性的和无序的执行,而不会在内核之外显示混乱,因此执行不必在缓存未命中存储上停止。缓存是连贯的;您无法从中读取“陈旧”的值,但是商店需要一些时间才能对其他内核可见。 (在计算机架构术语中,存储通过将数据+地址写入存储缓冲区来“执行”。当它从存储缓冲区提交到 L1d 缓存时,它在已知为非推测性后变得全局可见。)

核心需要在修改它之前获得缓存行的独占所有权(MESI Exclusive 或 Modified 状态),因此如果它还没有拥有该行,它会发送一个 RFO(Read For Ownership)需要将存储从存储缓冲区提交到 L1d 缓存。在核心看到 RFO 之前,它可以继续让负载读取该行(即“执行”负载 - 请注意,在高性能 CPU 中,负载和存储根本不同,核心希望尽可能早地加载数据,但是做迟到了)。

相关:如果线程 1 还进行了一些稍后的加载,即使在保持其他所有内容有序的强排序 CPU 上,存储缓冲区也是您获得 StoreLoad 重新排序的方式。或者在具有强排序内存模型(如 x86)的 CPU 上,保持一切都按程序顺序发生的错觉,但存储缓冲区除外。

内存屏障只是命令这个核心的操作。例如,一个完整的屏障会阻止稍后的加载执行,直到之前的存储+加载已经执行并且存储缓冲区已经耗尽到屏障的点,所以它只包含以后的加载(如果有的话)。

障碍对另一个核心是否看到商店没有影响,除非前提条件是另一个核心已经看到了一些其他商店。然后使用屏障(或等效的发布/获取),您可以保证其他核心也将看到发布存储之前的所有其他内容。


Jeff Preshing 的 mental model of memory operations as source-control operations 访问远程服务器是一个有用的模型:您可以订购自己的操作相对于彼此,但管道中的请求来自不同的核心可以以不同的顺序访问服务器(共享内存)。

这就是为什么 C++ 仅将可见性指定为“最终”/“立即”,如果您已经看到(通过获取加载)来自发布存储的值,则可以保证看到更早的内容。 (“迅速”的含义取决于硬件。在现代多核系统上通常低于 100 ns(取决于您正在测量的具体内容),尽管多插槽可能会更慢。If I don't use fences, how long could it take a core to see another core's writes?

查看存储本身(如果您不需要同步其他加载/存储,则发布、seq_cst 甚至放松)发生与否,这就是创建概念的原因线程之间的之前/之后。由于 CPU 只能通过共享内存(或处理器间中断)看到彼此的操作,因此没有很多好的方法可以建立任何同时性的概念。与物理学非常相似,如果两件事没有发生在同一个地方,相对论很难说它们同时发生:这取决于观察者,因为能够看到任何一个事件都有延迟。

(在诸如现代 x86 之类的机器上,TSC 在内核之间同步(这在单插槽多核系统中很常见,显然也是大多数(?)多插槽主板),您实际上可以找到绝对时间戳来确定哪个内核在什么时候执行什么,但是乱序执行仍然是一个很大的混淆因素。流水线 CPU 很难准确地说出任何给定指令“执行”的时间。而且由于通过内存进行通信不是零延迟,甚至尝试以这种方式建立同时性通常都没有用。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多