带有围栏和获取/释放的C ++ memory_order

时间:2013-01-24 01:11:59

标签: c++ multithreading c++11 synchronization

我有以下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'时会看到'false',因此不会在单位中增加z
  3. t1完成执行
  4. 在主线程中,断言失败,因为z.load()返回0
  5. 我认为这符合'获取' - '发布'规则,但是,例如在这个问题中的最佳答案:Understanding c++11 memory fences这与我的情况非常相似,它暗示类似于步骤1我的行动序列不能在'memory_order_release'之前发生,但是没有详细说明其背后的原因。

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

2 个答案:

答案 0 :(得分:4)

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

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

因为x的写入是在memory_order_release之前完成的,所以t2循环不会退出while(y...),直到x也为真。这意味着稍后读取x时,保证为1,因此更新z。我唯一的轻微问题是,如果您release z也不需要main,那么t1运行的处理器与t2z不同{1}},然后main可能在 ... mov $5, x cmp a, b jnz L1 mov $4, x 中有陈旧价值。

当然,如果你有一个多任务操作系统(或只是中断做足够的东西等),那就不是保证 - 因为如果运行t1的处理器获得其缓存刷新,那么t2可能会读取新的值X。

就像我说的那样,这对x86处理器(AMD或Intel处理器)没有影响。

因此,一般性地解释屏障指令(也适用于Intel和AMD process0rs):

首先,我们需要了解尽管指令可以无序启动和完成,但处理器确实对订单有一般性的“理解”。假设我们有这个“伪机器代码”:

mov $4, x

L1:      ......

处理器可以在完成“jnz L1”之前推测性地执行mov $4, x - 因此,为了解决这个问题,处理器必须在jnz L1回滚 mov $1, x wmb // "write memory barrier" mov $1, y 1}}被采取了。

同样,如果我们有:

{{1}}

处理器有规则说“在完成任何商店之前,不要执行任何商店指令”。这是一个“特殊”指令 - 它的目的是保证内存排序。如果它没有这样做,那么你的处理器就会坏掉,而设计部门的某个人就有“他的屁股”。

同样地,“读取存储器屏障”是由处理器的设计者保证在我们在屏障指令之前完成未决读取之前处理器将不完成另一次读取的指令。

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

通常还存在“隐含的内存障碍” - 也就是说,即使它们不存在也会导致内存障碍的指令。软件中断(“INT X”指令或类似指令)倾向于这样做。

答案 1 :(得分:2)

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

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

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

在t2中,我们在真正返回的载荷和获取障碍以及x.load之间发生了事件。

因为before-before是传递性的,所以释放障碍发生在获取障碍之前,而x.store发生在x.load之前。由于存在障碍,x.store与x.load同步 - 这意味着加载必须查看存储的值。

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