首页 > 解决方案 > 使用互斥锁的原子性 VS 同步

问题描述

int a = 0;

假设我们有 2 个线程访问共享内存a

Atomicity确保每个操作都fully done针对每个其他线程。因此,如果我让a = 5每个线程都看到05(并且没有另一半更新 32 位值)。

现在这是我的困惑。如果我想确保每个其他线程只5在上述分配之后看到。最流行的方法是使用locks我是否正确?

如果是这样,那么locks不提供原子性?原子性和同步是两个不同的概念?

标签: multithreadingshared-memoryatomic

解决方案


您实际描述的是另一个概念:可见性。

分配a=5时,其他 CPU 需要一段时间才能看到此分配。如果您希望分配线程在所有其他线程都看不到a == 0并且他们只能看到之前不继续执行a == 5,那么您需要一个内存屏障

因此,整理这些概念:

原子性

原子性是保证没有其他线程看到部分或暂时的状态变化,它们只看到一致的状态。

a == 5在 x86(以及一些但不是全部的其他体系结构)上, CPU 保证诸如此类的分配是原子的。

但是,如果您有一个带有 state 的结构{ a: 5, b: 10 },并且该结构的不变量是 that 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()有一个内存屏障,锁不仅自动保证了原子性,还保证了正确的可见性。


推荐阅读