【问题标题】:How effective a barrier is a atomic write followed by an atomic read of the same variable?原子写入后跟同一个变量的原子读取,屏障的有效性如何?
【发布时间】:2018-01-10 06:10:25
【问题描述】:

考虑以下几点:

#include <atomic>

std::atomic<unsigned> var;
unsigned foo;
unsigned bar;

unsigned is_this_a_full_fence() {
     var.store(1, std::memory_order_release);
     var.load(std::memory_order_acquire);
     bar = 5;
     return foo;
}

我的想法是 var 的虚拟负载应该防止 foo 和 bar 的后续变量访问在存储之前被重新排序。

似乎代码为重新排序设置了障碍 - 至少在 x86 上,发布和获取不需要特殊的屏蔽指令。

这是编写完整围栏(LoadStore/StoreStore/StoreLoad/LoadLoad)的有效方法吗?我错过了什么?

我认为该版本创建了 LoadStore 和 StoreStore 障碍。获取创建一个 LoadStore 和 LoadLoad 屏障。并且这两个变量访问之间的依赖关系创建了一个 StoreLoad 屏障?

编辑:将障碍更改为全围栏。制作 sn-p C++。

【问题讨论】:

  • 这不是 C++
  • 不清楚的问题,因为没有minimal reproducible example
  • 更新为 C++。
  • C++ 标准中是否有任何内容需要std::atomic&lt;&gt; 的原子性才能应用于所讨论的变量以外的任何内容?

标签: c++ multithreading memory-model memory-fences


【解决方案1】:

此代码的一个主要问题是存储和随后加载到同一内存位置显然没有与任何其他线程同步。在 C++ 内存模型中,竞争是未定义的行为,因此编译器可以假设您的代码没有竞争。您的负载可以观察到与存储的值不同的值的唯一方法是进行比赛。因此,在 C++ 内存模型下,编译器可以假设负载观察到存储的值。

这个确切的原子代码序列出现在我的 C++ 标准委员会论文 no sane compiler would optimize atomics 的“消除冗余负载”下。有a longer CppCon version of this paper on YouTube

现在想象一下,C++ 不是一个书呆子,尽管其固有的活泼性质,但加载/存储保证会保留在那里。现实世界的 ISA 提供了 C++ 所没有的此类保证。您通过获取/释放提供了与其他线程的一些发生之前的关系,但您没有提供所有线程都同意的唯一总顺序。所以是的,这将充当围栏,但它与获得顺序一致性,甚至是总商店订单不同。一些架构可能具有以明确定义但不同的顺序观察事件的线程。这对于某些应用程序来说非常好!您需要研究 IRIW(独立读取独立写入)以了解有关此主题的更多信息。 x86-TSO 论文在 ad-hoc x86 内存模型的上下文中专门讨论了它,并在各种处理器中实现。

【讨论】:

  • 谢谢!编译器怎么知道它没有同步?
  • FWIW,我用它来获取#StoreLoad 屏障——这样本地CPU 就不会在var.store 之前重新排序任何读取。这在商店指示确认收到通知的算法中很有用。我们希望确保看到通知之前的所有状态更改(对其他变量)。如果我能克服 C++ 未定义的行为问题,ISA 的东西可能足以支持它。
  • 负载获取需要在一个循环中,试图观察值的变化,以便与另一个线程同步。编译器知道情况并非如此,您显然没有与存储和加载之间的另一个线程同步。那是在 N4455 中。您可以在存储和负载之间放置一个std::atomic_signal_fence(std::memory_order_seq_cst)。这会生成零代码,但会告诉编译器你可能有一个信号改变了中间的东西,并且有点像asm volatile ("":::"memory");。这是超级粗略的,但在可预见的未来应该可以工作。
【解决方案2】:

您的伪代码(不是有效的 C++)作为一个整体不是原子的。

例如,context switch 可能发生在存储和加载之间,而其他一些线程将被调度(或已经在其他一些core 上运行),然后会更改其间的变量。上下文切换和interrupts 可以发生在每条机器指令中。

这是编写屏障的有效方法吗

不,不是。另请参阅pthread_barrier_init(3p)pthread_barrier_wait(3p) 和相关函数。

您应该阅读一些pthread tutorial(实际上,C++11 线程是它们之上的一个微小抽象)并考虑使用互斥锁。

注意std::memory_order 主要影响当前线程(以及它正在观察的内容),并且不要禁止它被中断/上下文切换...

另见this answer

【讨论】:

  • OP 指的是阻止内存重新排序的屏障类型(在无锁代码中使用的屏障)。 - pthread_barrier_wait 提供不同的功能
  • 但我的回答是:上下文切换可以在var.storevar.load 之间发生
  • 抱歉,我不清楚。我不是想给出一个跨线程同步原语的例子。我正在展示一个玩具示例,以尝试更好地理解 C++ 内存模型和栅栏。栅栏可能是一个比屏障更好的词。
【解决方案3】:

假设您在多个线程中运行此代码,使用这样的排序是不正确的,因为原子操作不同步(请参阅下面的链接),因此 foobar 不受保护。

但查看适用于单个操作的保证仍然可能具有一定的价值。
作为获取操作,var.load 不会与foobar 上的操作重新排序(线程间)(因此#LoadStore 和#LoadLoad,你是对的)。
但是,var.store 不受任何重新排序的保护(在此上下文中)。

#StoreLoad 重新排序可以通过标记两个原子操作seq_cst 来防止。在这种情况下,所有线程都将遵守定义的顺序(尽管仍然不正确,因为非原子不受保护)。

EDIT
var.store 不受重新排序保护,因为它对在它之前排序的操作(即程序顺序的早期)充当单向屏障,并且在您的代码中没有操作 在该存储之前。
var.load 充当在它之后排序的操作(即foobar)的单向屏障。

下面是一个基本示例,说明变量 (foo) 如何受原子存储/加载对保护:

// thread 1
foo = 42;
var.store(1, std::memory_order_release);

// thread 2
while (var.load(std::memory_order_acquire) != 1);
assert(foo == 42);

线程 2 仅在 观察到线程 1 设置的值之后继续。然后说存储已与加载同步,断言无法触发。

如需完整概述,请查看 Jeff Preshing 的 blog articles

【讨论】:

  • 我认为var.store 上的释放语义会阻止内存访问,然后再重新排序超过store。您能否详细说明:但是,var.store 不受任何重新排序的保护(在此上下文中)。
  • @ConstantineSapuntzakis 公平问题.. 我添加了一个示例
  • 谢谢!假设在调用is_this_a_full_fence() 之前存在存储和加载。这会改变你的答案吗?
  • @ConstantineSapuntzakis 它稍微改变了一点,因为在该函数之前的存储和加载无法使用var.store(release) 重新排序。但是,它仍然不正确,因为foobar 可以是不同线程同时访问,这是不允许的。我的示例展示了 foo 如何首先由线程 1 写入,然后由线程 2 读取。
猜你喜欢
  • 2017-08-12
  • 1970-01-01
  • 2015-07-16
  • 1970-01-01
  • 1970-01-01
  • 2016-04-04
  • 2015-08-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多