我为什么要明确地使操作成为原子?

时间:2012-03-05 18:56:19

标签: multithreading caching synchronization parallel-processing atomic

请考虑以下代码:

int x=0;

#pragma omp parallel num_threads(4) default(none) shared(x)
 {
  for(int i=0; i<1000; ++i)
   x++;
 }
cout << x << endl;

预期的输出为4000.但我通常看到的是2500-3500之间的东西。我已经知道为什么了,(因为我没有让这个操作成为原子)。直到今天,我认为这完全可以接受,但后来我想到了一些事情:

  

缓存一致性协议应该保持数据之间的一致性   核心。也就是说,如果核心想要写入变量,那么它必须首先   获得对它的独占访问权限,然后继续写操作。

现在我想知道为什么我会得到4000以外的任何结果,甚至    当我没有指定它是一个原子操作?

我想到的一件事是,当代码编译成机器代码时,它可能会创建两个x副本。

修改
我对缓存一致性协议的看法在下面的图here(第19页)中解释: Get Exclusive
现在我知道这个数字适用于使用位向量协议的多处理器(而不是多核)系统,但我认为接近这一点的是使用MESI协议的英特尔处理器。如果这是真的,那么在确认所有失效之前,读者将不会获得所请求值的副本。如我错了请纠正我。我已经尝试搜索MESI协议如何工作的细节,但我找不到太多。

3 个答案:

答案 0 :(得分:4)

我同意格雷的回答100%。但是,增量的非原子性是一个已知问题,它不仅适用于多核,因为它也可以在单个核心机器上发生。

事实是x++(通常)通过几个汇编指令实际完成,例如:

load r,[x]  ; load memory into register
incr r       ; increment register
stor [x],r  ; store register back to memory

所以虽然它是C程序中的单个操作,但它实际上是一个非原子的汇编指令序列,可以在任何时候中断。因此,即使在单个核心机器上,线程也可能在完成增量之前被中断,从而使变量处于不一致状态。

有些编译器或体系结构可能确实将增量视为原子,但假设这一点并不是一个好主意。

答案 1 :(得分:2)

为什么您认为值x存储在一致的缓存位置?每个核心都有自己的缓存,但除非你要求,否则不保证这些缓存之间的一致性。并且无法保证缓存更新的顺序 - 也不保证频率。一个线程可以向x添加100,然后缓存可以同步覆盖另一个线程的增量20。

第一次引用x时,它会从中央内存拉入处理器(或核心)内存缓存。最有可能每个线程第一次得到0。但是在循环的最后可能会将任何内容写回中央内存,并且每个线程都可以轻松地将1000写回x。肯定无法保证每个x都会更新x++ - 无论是书面还是重新阅读。事实上,除非进行同步,否则每次x 都不会更新。就这个紧密循环而言,x将永远不会从缓存中逐出,因此永远不会自动重新读取。即使它不是一个如此紧凑的循环,在猜测何时被驱逐x将是非常困难的 - 即使你总是在相同的硬件上工作。

最后,这个词真的是“同步”而不是“原子”。 x++现在很少是原子操作(它实际上是读取,增量,存储)但它在高速缓存存储器位置或中央存储器之间肯定不同步。

答案 2 :(得分:1)

高速缓存一致性意味着只要一个核心(或总线主控设备)写入内存位置,该位置就会在包含它的其他(所有)高速缓存中失效。这会强制他们在下次访问它(R或W)之前重新加载该位置(以64字节高速缓存行的形式)。

因此,缓存一致性不是数据一致性,它只是保证更新的位置将尽快失效。缓存不能做得更多,它们总是落后于执行核心,并且有点落后于彼此。如果一个核心更新了一个位置,而另一个核心稍稍稍微相同,那么两个相关的缓存都会认为它们的位置是有效的(并且它们都可能使彼此的缓存行无效)。

如果数据不能保证有效,这有什么保证?这是在这种情况下可以做到的最好的事情。选择是在完全同步的核心(运行速度非常慢)和全速运行缓存(具有特定的,定义的后果和处理它们的工作解决方案)之间。解决方案基本上是非常短的减速,这样一切都会在之后同步。这些间歇性的,非常短的减速应该与完全同步的核心的永久性减速进行权衡。

在正常情况下,不同核心或总线主控设备不会在同一位置发生争用。但是一旦他们开始共享某些内存位置,所提供的解决方案就可以让程序员确保可以实现必要的同步。

This seems like a pretty good paper on caches ... and this

编辑:更准确地说缓存一致性:当核心写入某个位置时,它自己的缓存系统将首先确保其他核心的缓存中的相关缓存信息无效。因此,在写入之后,写入该位置的核心的缓存将包含有关该位置的缓存数据。