【问题标题】:Atomicity VS Synchronization using a mutex使用互斥锁的原子性 VS 同步
【发布时间】:2019-11-16 16:17:25
【问题描述】:
int a = 0;

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

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

现在这是我的困惑。如果我想确保所有其他线程在上述分配之后只看到5 怎么办?最流行的方法是使用locks,我说对了吗?

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

【问题讨论】:

  • 锁提供同步的事实并不排除它还确保原子性的事实。是的,它们是不同的概念——原子性是单个操作的属性,而同步是一系列操作的属性。
  • 您应该包含一个语言标签或language-agnostic 标签。

标签: multithreading shared-memory atomic


【解决方案1】:

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

当您分配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()的实现有内存屏障,锁自动保证不仅原子性,而且正确的可见性。

【讨论】:

  • 为什么另一个 CPU 需要这么长时间才能看到该值?究竟哪个 CPU 需要这么长时间?
  • 这取决于具体的 CPU 实现,但一般的想法是任何 CPU 都会尽可能长地缓存它可以缓存的任何内容,并且只会考虑该值被另一个修改的可能性如果明确告知要考虑 CPU。
  • 哪个模型专门有一个缓存来保存过时的数据?
  • 据我所知,所有现代的。检测某个东西是否过时是很昂贵的,所以他们让程序员(好吧,库)决定什么时候是合理的。
猜你喜欢
  • 2012-08-28
  • 1970-01-01
  • 1970-01-01
  • 2011-06-10
  • 2022-01-18
  • 1970-01-01
  • 2014-05-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多