21

如果我有一个要从一个线程写入并从另一个线程读取的单个 int,我需要使用std::atomic, 以确保其值在内核之间是一致的,无论是否读取和写入它的指令在概念上是原子的。如果我不这样做,则可能是读取核心在其缓存中具有旧值,并且不会看到新值。这对我来说很有意义。

如果我有一些无法以原子方式读取/写入的复杂数据类型,我需要使用一些同步原语来保护对它的访问,例如std::mutex. 这将防止对象进入(或被读取)不一致的状态。这对我来说很有意义。

对我来说没有意义的是互斥锁如何帮助解决原子解决的缓存问题。它们的存在似乎只是为了防止对某些资源的并发访问,而不是将包含在该资源中的任何值传播到其他核心的缓存。我是否遗漏了一些处理这个问题的语义?

4

3 回答 3

6

内存屏障(这也防止指令重新排序)确保了内核之间的一致性。当您使用 时std::atomic,您不仅可以原子地访问数据,而且编译器(和库)还会插入相关的内存屏障。

互斥锁的工作方式相同:互斥锁实现(例如 pthreads 或 WinAPI 或其他)在内部也插入内存屏障。

于 2013-06-11T14:29:23.213 回答
6

对此的正确答案是魔法小精灵——例如它只是有效。每个平台的 std::atomic 实现必须做正确的事情。

正确的是 3 个部分的组合。

首先,编译器需要知道它不能跨边界移动指令[事实上在某些情况下它可以,但假设它没有]。

其次,缓存/内存子系统需要知道——这通常是使用内存屏障完成的,尽管 x86/x64 通常具有如此强大的内存保证,因此在绝大多数情况下这不是必需的(这是一个很大的耻辱,因为它很好错误的代码实际上会出错)。

最后,CPU 需要知道它不能重新排序指令。现代 CPU 在重新排序操作方面非常积极,并确保在单线程情况下这是不明显的。他们可能需要更多提示,这在某些地方不会发生。

对于大多数 CPU,第 2 部分和第 3 部分归结为同一件事 - 内存屏障意味着两者。第 1 部分完全在编译器内部,由编译器编写者负责。

有关更多有趣的信息,请参阅 Herb Sutters 谈论“原子武器” 。

于 2013-06-11T15:01:35.430 回答
5

大多数现代多核处理器(包括 x86 和 x64)都是缓存一致的。如果两个核心在缓存中拥有相同的内存位置并且其中一个更新了值,则更改会自动传播到其他核心的缓存。它效率低下(从两个内核同时写入同一个缓存行真的很慢)但是没有缓存一致性,编写多线程软件将非常困难。

就像 syam 所说,内存屏障也是必需的。它们阻止编译器或处理器重新排序内存访问,并且还强制写入内存(或至少写入缓存),例如,当由于编译器优化而将变量保存在寄存器中时。

于 2013-06-11T15:00:28.607 回答