每个 memory_order 是什么意思?

新手上路,请多包涵

我读了一章,我不太喜欢它。我仍然不清楚每个内存顺序之间有什么区别。这是我目前的猜测,在阅读了更简单的 http://en.cppreference.com/w/cpp/atomic/memory_order 后我理解了

以下是错误的,所以不要试图从中学习

  • memory_order_relaxed:不同步,但在不同原子变量中从另一个模式完成订单时不会被忽略
  • memory_order_consume:同步读取这个原子变量,但是它不同步在此之前编写的宽松变量。但是,如果线程在修改 Y(并释放它)时使用 var X。其他消耗 Y 的线程也会看到 X 被释放?我不知道这是否意味着这个线程推出了 x(显然是 y)的变化
  • memory_order_acquire:同步读取这个原子变量,并确保在此之前写入的宽松变量也被同步。 (这是否意味着所有线程上的所有原子变量都已同步?)
  • memory_order_release:将原子存储推送到其他线程(但前提是它们使用消耗/获取读取 var)
  • memory_order_acq_rel:用于读/写操作。是否进行获取,以便您不修改旧值并释放更改。
  • memory_order_seq_cst:与获取释放相同,除了它强制在其他线程中看到更新(如果 a 在另一个线程上轻松存储。我存储 b 与 seq_cst。第三个线程阅读 a 放松会看到随着 b 和任何其他原子变量的变化?)。

我想我理解但如果我错了请纠正我。我找不到任何用易于阅读的英语解释它的东西。

原文由 user34537 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 775
2 个回答

GCC Wiki 通过代码示例提供了 非常全面且易于理解的解释

(摘录已编辑,并添加了重点)

重要的:

在将我自己的措辞添加到答案的过程中重新阅读从 GCC Wiki 复制的以下引用时,我注意到引用实际上是错误的。他们以完全错误的方式 获取 和 _消费_。 release-consume 操作仅提供对依赖数据的排序保证,而 release-acquire 操作提供该保证,而不管数据是否依赖于原子值。

第一个模型是“顺序一致的”。这是未指定时使用的默认模式,也是最严格的。它也可以通过 memory_order_seq_cst 明确指定。它提供 了与顺序程序员天生熟悉的相同的限制和限制移动负载,除了它适用于跨线程

[…]

从实际的角度来看,这相当于所有原子操作都充当了优化障碍。可以在原子操作之间重新排序,但不能跨操作重新排序。线程本地的东西也不受影响,因为其他线程看不到。 […] 这种模式还提供了 所有 线程的一致性。

相反的方法memory_order_relaxed 。该模型通过删除发生前的限制来减少同步。这些类型的原子操作还可以对其执行各种优化,例如死存储删除和共享。 […] 如果没有任何发生前的边缘,任何线程都不能指望来自另一个线程的特定排序。

当程序员只是希望变量本质上是原子的而不是使用它来同步线程以获取其他共享内存数据时,最常使用 宽松模式。

第三种模式( memory_order_acquire / memory_order_release )是其他两种模式的 混合。获取/释放模式类似于顺序一致模式,只是它只 对因变量应用发生前的关系。这允许放宽独立读取和独立写入之间所需的同步。

memory_order_consume 是对释放/获取内存模型的进一步细微改进,它通过 在对非依赖共享变量进行排序之前删除发生的事件 稍微放宽了要求。

[…]

真正的区别归结为硬件必须刷新多少状态才能同步。由于消费操作 可能 因此执行得更快,因此知道自己在做什么的人可以将其用于性能关键的应用程序。

以下是我自己尝试的更平凡的解释:

另一种看待它的方法是从重新排序读取和写入的角度来看待问题,包括原子的和普通的:

所有 原子操作都保证在其自身内部是原子的( 两个 原子操作的组合不是一个整体的原子!)并且在它们出现在执行流的时间轴上的总顺序中是可见的。这意味着在任何情况下都不能对原子操作进行重新排序,但其他内存操作很可能会被重新排序。编译器(和 CPU)通常会进行这种重新排序作为优化。

这也意味着编译器必须使用任何必要的指令来保证在任何时候执行的原子操作将看到之前执行的每个其他原子操作的结果,可能在另一个处理器内核(但不一定是其他操作)上.

现在, 放松 就是这样,最低限度。除此之外,它什么也不做,也不提供任何其他保证。这是最便宜的手术。对于强排序处理器架构(例如 x86/amd64)上的非读-修改-写操作,这归结为一个普通的普通移动。

顺序一致 的操作正好相反,它不仅对原子操作强制执行严格的排序,而且对之前或之后发生的其他内存操作也强制执行严格的排序。任何人都无法跨越原子操作强加的障碍。实际上,这意味着失去优化机会,并且可能必须插入栅栏指令。这是最昂贵的型号。

释放 操作防止普通加载和存储在原子操作 之后 重新排序,而 获取 操作防止普通加载和存储在原子操作 之前 重新排序。其他一切仍然可以移动。

防止存储在之后移动以及在相应原子操作之前移动负载的组合确保了获取线程看到的任何内容都是一致的,只有少量的优化机会丢失。

人们可能会认为这就像一个不存在的锁,它正在被释放(由作者)和获取(由读者)。除了…没有锁。

在实践中,发布/获取通常意味着编译器不需要使用任何特别昂贵的特殊指令,但它 不能 随意重新排序加载和存储,这可能会错过一些(小)优化机会。

最后, consumeacquire 是相同的操作,只是排序保证仅适用于依赖数据。相关数据将是例如由原子修改的指针指向的数据。

可以说,这可能会提供一些获取操作不存在的优化机会(因为更少的数据受到限制),但是这是以更复杂和更容易出错的代码和非平凡任务为代价的获得正确的依赖链。

目前不鼓励在规范修订时使用 消费 排序。

原文由 Damon 发布,翻译遵循 CC BY-SA 3.0 许可协议

这是一个相当复杂的课题。尝试阅读 http://en.cppreference.com/w/cpp/atomic/memory_order 几次,尝试阅读其他资源等。

这是一个简化的描述:

编译器 CPU 可以重新排序内存访问。也就是说,它们的发生顺序可能与代码中指定的顺序不同。这在大多数情况下都很好,当不同的线程尝试通信并且可能会看到这样的内存访问顺序破坏了代码的不变量时,就会出现问题。

通常您可以使用锁进行同步。问题是它们很慢。原子操作要快得多,因为同步发生在 CPU 级别(即 CPU 确保没有其他线程,即使在另一个 CPU 上,也不会修改某些变量等)。

因此,我们面临的一个问题是内存访问的重新排序。 memory_order 枚举指定了编译器 必须 禁止的重新排序类型。

relaxed - 没有限制。

consume - 任何依赖于新加载值的负载都不能重新排序。原子负荷。即,如果它们在源代码中的原子加载之后,它们也会在原子加载之后 _发生_。

acquire - 没有负载可以重新排序。原子负荷。即,如果它们在源代码中的原子加载之后,它们也会在原子加载之后 _发生_。

release - 没有商店可以重新排序。原子商店。即,如果它们在源代码中的原子存储之前,它们也会在原子存储之前 _发生_。

acq_rel - acquirerelease 组合。

seq_cst - 更难理解为什么需要这种排序。基本上,所有其他排序仅确保特定不允许的重新排序不会仅发生在消耗/释放相同原子变量的线程中。内存访问仍然可以以任何顺序传播到其他线程。这种排序确保不会发生这种情况(因此是顺序一致性)。对于需要这样做的情况,请参阅链接页面末尾的示例。

原文由 user283145 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题