多核时代下线程间的内存可见性

19 篇文章 4 订阅
14 篇文章 1 订阅

目录

一  源起

二  互斥锁和内存可见性

三 原子操作和内存可见性

四  join

五  cpu的乱序执行

六  参考文章


一  源起

       现代多核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的缓存中的数据写回到主内存中,同时使得其他CPU的缓存中的数据失效。这样,当其他线程在获取锁时,就必须从主内存中重新读取数据,从而可以看到最新的数据。当我们释放锁时,也会执行一个内存屏障操作,这个操作同样会强制将CPU的缓存中的数据写回到主内存中,同时使得其他CPU的缓存中的数据失效。这样,当其他线程在获取锁时,就必须从主内存中重新读取数据,从而可以看到最新的数据。因此,通过内存屏障,我们可以保证在锁保护下的数据修改对所有线程可见。

      这里简单说下内存屏障(Memory Barrier),后面会细说,它是一种同步机制,用于确保在多核处理器系统中的内存一致性。内存屏障可以强制处理器在执行内存屏障指令之前的所有内存访问(读取和写入)操作都完成,并且在该指令之后的所有内存访问操作都未开始执行。在内存屏障前的内存修改,如果没有其他同步机制(如锁或原子操作),那么在其他线程可能无法立即看到这些修改。这是因为处理器可能会对内存访问操作进行缓存和优化,导致实际的内存访问顺序与程序代码的顺序不同。这种现象被称为内存重排序(Memory Reordering)。所以,如果你希望在内存屏障前的内存修改在其他线程立即可见,你需要使用适当的同步机制来确保内存的可见性。

三 原子操作和内存可见性

       我们重点说原子操作的内存可见性,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看到的可能是一个旧的、未修改的值。

四  join

       线程的join操作本身并不会刷新CPU缓存,但是,当一个线程结束并且另一个线程在等待它(通过调用join)时,操作系统会确保所有的内存操作都已经完成,这包括将缓存中的数据写回到主内存。这是由于在多线程环境中,操作系统和硬件会遵循一些内存模型的规则,以确保内存的一致性和可见性。但是,这并不意味着你可以依赖线程的join操作来同步内存,因为join会阻塞当前的线程。如果你需要在多线程环境中共享数据,你应该使用适当的同步原语,如互斥锁或原子操作。

五  cpu的乱序执行

      指令的乱序执行除了编译器的优化外,cpu执行指令的过程也可能导致乱序执行。CPU的乱序执行主要是由于CPU的流水线架构和优化策略。流水线架构允许CPU同时处理多个指令,这些指令可能并不是按照它们在程序中出现的顺序执行的。这种优化策略可以提高CPU的执行效率,但也可能导致指令的执行顺序与程序中的顺序不一致,这就是所谓的乱序执行。

      所以,CPU的乱序执行和缓存一致性问题都可能导致多线程程序中的数据竞争和同步问题。为了解决这些问题,我们需要使用内存屏障、锁、原子操作等同步机制。

揭开内存屏障的面纱

c++原子操作的各种内存序

六  参考文章

mutex如何保证数据的一致性和正确性

 c++ std::atomic 数据可见性

C++的std::atomic与体系结构中多核内存模型/缓存一致性协议有什么关系?

Memory Model: 从多处理器到高级语言
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值