目录
[一 源起](#一 源起)
[二 互斥锁和内存可见性](#二 互斥锁和内存可见性)
[三 原子操作和内存可见性](#三 原子操作和内存可见性)
[四 内存序](#四 内存序)
[五 参考文章](#五 参考文章)
一 源起
现代多核CPU通常具有多级缓存(L1、L2、L3等),每个核心可能有自己的私有L1和L2缓存,而L3缓存可能是共享的。在多核处理器系统中,每个核心可能会在其本地缓存中存储内存位置的副本。这可能导致一个核心上的线程修改了数据,而这个修改没有立即反映到其他核心的缓存中,从而导致缓存不一致为了解决这个问题,现代CPU使用缓存一致性协议(如MESI协议),确保多个CPU核心之间的缓存保持一致。当一个核心修改了它的缓存中的数据时,其他核心的缓存副本将被标记为无效,并在需要时从主内存中重新加载最新数据。这个过程就是保证了当一个线程修改了一个变量的值时其他线程可以立刻访问到修改后的值。
二 互斥锁和内存可见性
mutex使我们最常用的互斥保护方式,使用mutex(互斥锁)可以确保在多线程环境下对共享数据的安全访问。当一个线程获取了mutex锁并修改了共享数据后,其他线程在获取到mutex锁后可以立刻看到更新后的值。这是因为mutex除了提供互斥访问的能力外,还有一个重要的特性就是内存屏障(Memory Barrier)。内存屏障可以防止CPU的指令重排,确保在mutex锁释放之前的所有内存写入操作都对其他线程可见。这就保证了当其他线程在获取到mutex锁后,可以立刻看到更新后的值。但是需要注意的是,这个行为是针对mutex锁的保护下的操作。如果有线程在没有获取mutex锁的情况下访问共享数据,那么就可能看到的是旧的数据,因为这种情况下没有内存屏障的保护。
由此我们可以看到std::mutex本身并不会刷新CPU缓存。然而,当你在多线程环境中使用std::mutex来保护数据时,它可以确保在锁的保护下,对数据的修改对所有线程都是可见的。这是因为在获取和释放锁的过程中,会进行内存屏障操作,这将强制刷新CPU缓存,使得在锁保护下的数据修改对所有线程可见。
三 原子操作和内存可见性
我们重点说原子操作的内存可见性,c++11提供了std::atomic原子操作,std::atomic提供了一种在多线程环境中对数据进行原子操作的方式。原子操作是不可中断的操作,一旦开始就会执行到结束,不会被其他线程打断。这就保证了在多线程环境中,使用std::atomic进行操作的数据在任何时刻都是一致的,不会出现因为线程切换导致的数据不一致的问题,保证一个变量写到一半儿被其他线程读到。并且std::atomic操作也会刷新CPU缓存。这是因为std::atomic提供了一种机制,确保在多线程环境中对特定对象的操作是原子的,即不可中断的。这意味着,一旦一个线程开始一个原子操作,它将在任何其他线程有机会访问该对象之前完成该操作。所以在多核处理器系统中,当一个原子操作完成时,处理器会刷新其缓存,以便其他处理器可以看到最新的数据。然而,这并不意味着所有的std::atomic操作都会导致缓存刷新。具体行为取决于所使用的内存顺序(memory order)。例如,std::memory_order_relaxed就不会导致缓存刷新,而std::memory_order_acquire、std::memory_order_release、std::memory_order_acq_rel和std::memory_order_seq_cst则会。
所以并不是所有的std::atomic操作都有内存可见性的保证,其中std::memory_order_relaxed是最弱的内存序,它只保证了原子操作本身不会被重排,但不保证操作之间的顺序。也就是说,如果线程A在std::memory_order_relaxed模式下修改了一个std::atomic变量,线程B可能看不到这个修改,或者说,线程B看到的可能是一个旧的、未修改的值。
四 内存序
最后说到另一个更加复杂的问题内存序,关于内存序前面已经有几个文章讨论这个问题