如果我有一个int要从一个线程写入并从另一个线程读取,我需要使用std::atomic
,以确保其值在核心之间保持一致,无论是否从中读取和写入的指令在概念上是原子的。如果不这样做,可能是读取内核的缓存中有旧值,并且不会看到新值。这对我来说很有意义。
如果我有一些无法以原子方式读/写的复杂数据类型,我需要使用某些同步原语(例如std::mutex
)来保护对它的访问。这将防止对象进入(或被读取)不一致的状态。这对我来说很有意义。
对我来说没有意义的是互斥体如何帮助解决原子论所解决的缓存问题。它们似乎只是为了防止对某些资源的并发访问,而不是将该资源中包含的任何值传播到其他核心的缓存。是否有一些我错过的语义部分涉及到这个问题?
答案 0 :(得分:6)
memory barriers (这也可以防止指令重新排序)确保核心之间的一致性。使用std::atomic
时,不仅会以原子方式访问数据,而且编译器(和库)也会插入相关的内存障碍。
互斥体以相同的方式工作:互斥实现(例如,pthreads或WinAPI或者不是)在内部也插入内存障碍。
答案 1 :(得分:5)
对此的正确答案是神奇的小精灵 - 例如它只是工作。每个平台的std :: atomic实现必须做正确的事。
正确的是3个部分的组合。
首先,编译器需要知道它不能跨越边界移动指令[实际上它可以在某些情况下,但假设它没有]。
其次,缓存/内存子系统需要知道 - 这通常是使用内存屏障来完成的,尽管x86 / x64通常具有如此强大的内存保证,这在绝大多数情况下都不是必需的(这是一个很大的耻辱)因为错误的代码实际上出错了很好。)
最后,CPU需要知道它无法重新排序指令。现代CPU在重新排序操作方面非常积极,并确保在单线程情况中这是不明显的。他们可能需要更多提示,这在某些地方不会发生。
对于大多数CPU而言,第2部分和第3部分归结为相同的事情 - 内存屏障意味着两者兼而有之。第1部分完全在编译器内部,并且由编译器编写者来完成。
请参阅Herb Sutters谈论'Atomic Weapons'以获取更多有趣的信息。
答案 2 :(得分:4)
大多数现代多核处理器(包括x86和x64)都是cache coherent。如果两个核心在高速缓存中保留相同的内存位置,并且其中一个核心更新了该值,则更改将自动传播到其他核心的高速缓存。这是低效的(从两个内核同时写入相同的缓存行真的很慢)但是没有缓存一致性,编写多线程软件将非常困难。
和syam说的一样,内存障碍也是必需的。它们会阻止编译器或处理器重新排序内存访问,并且还会强制写入内存(或至少进入缓存),例如由于编译器优化而将变量保存在寄存器中。