【问题标题】:Is a read on an atomic variable guaranteed to acquire the current value of it in C++11?对原子变量的读取是否保证在 C++11 中获取它的当前值?
【发布时间】:2014-08-13 15:35:18
【问题描述】:

众所周知,对单个原子变量的修改形成一个全序。假设我们在挂钟时间 T 对某个原子变量v 进行了原子读取操作。那么,这个读取是否保证获取v 的当前值,它是由@987654323 的修改顺序中的最后一个写入的@ 在时间 T?换句话说,如果在自然时间里,原子写是在原子读之前完成的,并且中间没有其他写,那么读是否保证返回刚刚写的值?

我接受的答案是 Cubbi 对他的回答发表的第 6 条评论。

【问题讨论】:

  • 这不是原子的全部意义吗?
  • 在研究了记忆顺序可能涉及的复杂性之后,现在对我来说似乎并不明显。
  • @stijn 并行机器翻译的重点不就是没有全局同步时间吗?
  • @curiousguy '整个'点我不会说,但 AFAICT OP 使用墙上时间的概念只是为了表达之前/之后的概念
  • @stijn 之前/之后仅在给定线程(以及该线程中的信号)上完全定义。在线程之间有不同的非常抽象的顺序,它们必须与线程匹配,有时还必须相互匹配......

标签: memory c++11 synchronization atomic


【解决方案1】:

挂钟时间无关紧要。但是,您所描述的内容听起来像是读写一致性保证:

$1.10[intro.multithread]/20

如果原子对象M 上的副作用X 发生在M 的值计算B 之前,则评估B 应从X 或副作用@ 中获取其值987654328@ 跟在X 之后,修改顺序为M

(翻译标准,“值计算”是读取,“副作用”是写入)

特别是,如果您的轻松写入和轻松读取在同一函数的不同语句中,它们通过先序关系连接,因此它们通过发生之前关系连接,因此保证成立。

【讨论】:

  • 我的问题在于,当值计算读取写入值时,写入线程发生在读取之前。但是是否有任何条件在满足时实际上会发生(否则,这可能永远不会发生)?我正在考虑是否发生在挂钟时间之前是这样的条件。
  • 必要条件是happens-before的定义。它不涉及挂钟。
  • 我认为下面的例子可能会让我的问题更清楚。考虑由原子自旋锁同步的两个线程。线程 A 首先获取锁。然后线程 B 正忙于等待。接下来,线程 A 释放锁。是否有关于线程 B 何时看到线程 A 的此版本并因此进入临界区的任何线索,或者它可能只是等待无限长才能看到此版本?
  • @Lingxi,线索是内存顺序语义。 A 中的存储必须至少为memory_order_release,负载必须为memory_order_acquire
  • @Lingxi 29.3[atomics.order]/12 “实现应该使原子存储在合理的时间内对原子负载可见。”
【解决方案2】:

取决于您可以为load() 操作指定的内存顺序。

默认是std::memory_order_seq_cst 答案是肯定的保证)。

但是,如果您为加载操作指定 std::memory_order_relaxed,则 documentation 会显示为 Relaxed ordering: there are no synchronization or ordering constraints, only atomicity is required of this operation。 IE。程序最终可能根本不从内存中读取。

【讨论】:

  • 我不认为memory_order_seq_cst 实际上能保证这一点。它确实保证了一个全球性的总秩序存在,但没有说清楚这个秩序是如何形成的,即构建这种秩序的规则。
  • 内存顺序的规则描述。实现机制是防止在编译器和处理器端的原子加载(或存储)之前和之后的操作的省略和重新排序。从而保证存储将发生在正确的位置,加载将在其他操作中发生在正确的位置;处理器将执行这些操作。
  • @Lingxi 一致性意味着任何原子操作的顺序(从 seq_cst 到松弛)必须遵循给定线程中的执行顺序(您不会及时返回);多个线程对相同对象的获取和释放将操作顺序“绑定”在一起,并迫使它们继续进行。 OTOH,如果您没有在线程之间共享原子对象,那么您就没有这种联系。但那你为什么要关心抽象顺序呢?
  • "它确实保证存在一个全局总顺序" 不相关线程内不共享任何操作的全局顺序是一个没有物理对应物的抽象智能设备。您必须考虑通过原子对象进行交互的线程:实际上,这些线程将使用按某种顺序可见的栅栏来实现,并且原子存储被实现为将缓存行置于独占修改状态的真实内存存储,该状态确定该缓存行中对象的修改顺序。栅栏使这些修改全局一致。
【解决方案3】:

对原子变量的读取是否保证获取它的当前值

没有

即使每个原子变量都有一个修改顺序(所有线程都观察到),但这并不意味着所有线程都在同一时间尺度上观察到修改。

考虑这段代码:

std::atomic<int> g{0};

// thread 1
g.store(42);

// thread 2
int a = g.load();
// do stuff with a
int b = g.load();

可能的结果是(见图):

  • 线程 1:42 存储在时间 T1
  • 线程 2:第一次加载在 T2 时间返回 0
  • 线程 2:来自线程 1 的存储在时间 T3 变为可见
  • 线程 2:第二次加载在 T4 时间返回 42。

即使在时间 T2 的第一次加载发生在 T1 的存储之后(按时钟时间),这种结果也是可能的。

标准说:

实现应该使原子存储在合理的时间内对原子负载可见。

它不需要 store 立即变得可见,它甚至允许 store 保持不可见的空间(例如,在没有缓存一致性的系统上)。 在这种情况下,需要原子读取-修改-写入 (RMW) 来访问最后一个值。

原子读-修改-写操作应始终读取最后写入的值(按修改顺序) 在与读-修改-写操作关联的写之前。

不用说,RMW 的执行成本更高(它们会锁定总线),这就是允许常规原子加载返回旧(缓存)值的原因。
如果需要定期加载来返回最后一个值,那么性能会很糟糕,而几乎没有任何好处。

【讨论】:

  • 在哪种实际情况下,常规加载可以返回旧的过时值?旧值存储在哪里?
  • @curiousguy 在缓存中,如果它不连贯。如果存储的值(其他核心)仍在存储缓冲区(X86)中,您也可以考虑加载已过时。问题是常规存储和加载没有当前或最新值的定义。最新的修改顺序真的只适用于RMW操作
  • 哪种现代架构(具有某种形式的 C++ 或类似 C++ 的编译器)具有非一致缓存?
  • @curiousguy 不知道,但缓存一致性不是强制性功能
  • 在我看来,一致性对于以合理的效率支持任何常见的 PL 都是必不可少的......在没有缓存一致性的理论系统上至少有一个 C++ 原型编译器吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-18
  • 2018-07-21
  • 2016-02-14
  • 2011-02-13
相关资源
最近更新 更多