9

我有以下 C++ 2011 代码:

std::atomic<bool> x, y;
std::atomic<int> z;

void f() {
   x.store(true, std::memory_order_relaxed);
   std::atomic_thread_fence(std::memory_order_release);
   y.store(true, std::memory_order_relaxed);
}

void g() {
   while (!y.load(std::memory_order_relaxed)) {}
   std::atomic_thread_fence(std::memory_order_acquire);
   if (x.load(std::memory_order_relaxed)) ++z;
}

int main() {
   x = false;
   y = false;
   z = 0;
   std::thread t1(f);
   std::thread t2(g);
   t1.join();
   t2.join();
   assert(z.load() !=0);
   return 0;
}

在我的计算机体系结构课上,我们被告知这段代码中的断言总是成真。但是现在仔细审查后,我真的不明白为什么会这样。

据我所知:

  • 带有“ memory_order_release ”的栅栏将不允许在它之后执行先前的存储
  • 带有“ memory_order_acquire ”的栅栏将不允许在它之后执行的任何加载在它之前执行。

如果我的理解是正确的,为什么下面的动作序列不能发生?

  1. 在 t1 内,y.store(true, std::memory_order_relaxed);被称为
  2. t2 完全运行,并且在加载“x”时会看到“假”,因此不会增加单位中的 z
  3. t1 完成执行
  4. 在主线程中,断言失败,因为 z.load() 返回 0

我认为这符合 'acquire'-'release' 规则,但是,例如在这个问题的最佳答案中:Understanding c++11 memory fences这与我的情况非常相似,它暗示我的第 1 步在“memory_order_release”之前不能发生一系列动作,但没有详细说明其背后的原因。

我对此感到非常困惑,如果有人能对此有所了解,我将非常高兴:)

4

2 回答 2

4

每种情况下发生的具体情况取决于您实际使用的处理器。例如,x86 可能不会对此断言,因为它是一个缓存一致的架构(您可以有竞争条件,但是一旦一个值从处理器写入缓存/内存,所有其他处理器都会读取该值 -当然,不会阻止另一个处理器立即写入不同的值,等等)。

因此,假设这是在 ARM 或类似的处理器上运行的,但它本身不能保证缓存一致:

因为写入x是在 之前完成的memory_order_release,所以 t2 循环不会退出while(y...)untilx也是如此。这意味着当x稍后被读取时,它保证为 1,因此z被更新。我唯一的小问题是你是否也不需要releasefor z......如果在与andmain不同的处理器上运行,那么在.t1t2zmain

当然,如果你有一个多任务操作系统(或者只是做足够多的事情的中断等),这并不能保证发生——因为如果运行 t1 的处理器刷新了它的缓存,那么 t2 很可能会读取 x 的新值。

就像我说的,这不会对 x86 处理器(AMD 或 Intel 处理器)产生这种影响。

所以,一般来说解释屏障指令(也适用于 Intel 和 AMD process0rs):

首先,我们需要明白,虽然指令可以乱序开始和结束,但处理器确实对顺序有一个普遍的“理解”。假设我们有这个“伪机器代码”:

 ...
 mov $5, x
 cmp a, b
 jnz L1
 mov $4, x

L1:...

处理器可以mov $4, x在完成“jnz L1”之前推测性地执行 - 因此,为了解决这个事实,处理器必须回滚在被采取mov $4, x的情况下。jnz L1

同样,如果我们有:

 mov $1, x
 wmb         // "write memory barrier"
 mov $1, y

处理器有规则说“在完成之前的所有存储之前,不要执行在 wmb 之后发出的任何存储指令”。这是一条“特殊”指令——它的存在是为了保证内存排序的确切目的。如果它不这样做,那么你的处理器就坏了,而设计部门的某个人“他的屁股就在线”。

同样,“读取内存屏障”是一条指令,处理器的设计者保证处理器不会完成另一次读取,直到我们完成屏障指令之前的未决读取。

只要我们不在“实验”处理器或一些不能正常工作的肮脏芯片上工作,它就会以这种方式工作。它是该指令定义的一部分。如果没有这样的保证,就不可能(或至少极其复杂和“昂贵”)实现(安全)自旋锁、信号量、互斥锁等。

通常还存在“隐式内存屏障”——即即使没有导致内存屏障的指令。软件中断(“INT X”指令或类似指令)倾向于这样做。

于 2013-01-24T01:23:22.873 回答
3

我不喜欢用“这个处理器做这个,那个处理器做那个”来争论 C++ 并发问题。C++11 有一个内存模型,我们应该使用这个内存模型来确定什么是有效的,什么是无效的。CPU 架构和内存模型通常更难理解。另外还有不止一个。

考虑到这一点,考虑一下:线程 t2 在 while 循环中被阻塞,直到 t1 执行 y.store 并且更改已传播到 t2。(顺便说一句,这在理论上可能永远不会发生。但这不现实。)因此,我们在 t1 中的 y.store 和 t2 中的 y.load 之间有一个发生前的关系,允许它离开循环。

此外,我们在 x.store 和释放屏障以及屏障和 y.store 之间有简单的线程内发生前关系。

在 t2 中,我们在真正返回的负载和获取屏障和 x.load 之间有一个发生前。

因为happens-before是可传递的,所以release屏障发生在acquire屏障之前,而x.store发生在x.load之前。由于障碍,x.store 与 x.load 同步,这意味着负载必须查看存储的值。

最后,z.add_and_fetch(post-increment)发生在线程终止之前,也就是在主线程从t2.join唤醒之前发生,也就是在主线程中的z.load之前发生,所以对z的修改必须在主线程中可见。

于 2013-01-24T12:35:04.110 回答