您实际上描述的是另一个概念:可见性。
当您分配a=5 时,其他CPU 需要一段时间才能看到此分配。如果您希望分配线程在所有其他线程都看不到a == 0 之前不继续执行,而他们只能看到a == 5,那么您需要一个memory barrier。
所以整理一下这些概念:
原子性
原子性保证没有其他线程看到部分或暂时的状态变化,它们只看到一致的状态。
在 x86(以及一些但不是全部的其他架构)上,CPU 保证诸如 a == 5 之类的赋值是原子的。
但是如果你有一个状态为{ a: 5, b: 10 } 的结构,并且该结构的不变量是b == a * 2,为了改变状态,你需要2 个赋值,这不是原子的。在这种情况下,要使状态更改原子化,您需要锁。
锁
锁允许您在线程之间实现一个协议,以便它们等到状态一致后再访问它。
在上面的例子中,实现方式是lock(mystruct); mystruct.a = myarg; mystruct.b = myarg * 2; unlock(mystruct);。 lock(mystruct)的目标是让线程一直等到mystruct的状态一致,从而手动实现原子性。
可见性
CPU 缓存了很多东西。如果每次分配变量时 CPU 都必须写入主存,那么至少会慢数千倍。
此外,CPU 重新排序指令以获得最佳执行速度。
因此分配将最终对其他线程可见,但不是立即可见的,也不是按顺序进行的。
如果你想要更强的保证,你需要一个内存屏障。
内存屏障
看下面的代码:
lock(mystruct);
mystruct.a = 9;
mystruct.b = 18;
unlock(mystruct);
// some thread might interleave here
lock(mystruct);
print(mystruct.a);
print(mystruct.b);
unlock(mystruct);
CPU 可能会说:“好吧,我只是设置了mystruct.a == 9,所以我可以打印 9,我不需要从主内存中读取mystruct.a”
为了防止这种情况,unlock(mystruct) 的实现通常包含一个内存屏障。
内存屏障阻止 CPU 假设在屏障之前发生的任何事情在屏障之后仍然有效,因此当它需要打印 struct.a 时,它会从主内存中获取它。
把它绑在一起
因为unlock()的实现有内存屏障,锁自动保证不仅原子性,而且正确的可见性。