内存屏障的前世今生
从cpu的发展进程讲起
早期单核cpu
特点: 单核,按照程序指令顺序进行计算
性能优化:
- 乱序执行:让 CPU 可以不按程序的顺序执行指令,从而利用 CPU 的空闲资源,提升执行效率(重排序)
- 提升主频(由于功耗和发热的限制,单纯提高主频难以持续提升性能)等等
多核cpu的出现
特点:同一个处理器多个核心
性能提升方式:让多个任务同时运行,从而提升整体性能
多核导致的问题
-
缓存不一致问题
如果各个核心的缓存数据不同步,就会导致数据不一致问题
MESI
缓存一致性协议,解决多核下缓存不一致的问题,最常见的缓存一致性方案
- Modified (M) :缓存中的数据被修改,且与主内存中的数据不同。
- Exclusive (E) :缓存中的数据与主内存相同,且只有该核心拥有此数据。
- Shared (S) :多个核心的缓存中都可能有相同的数据。
- Invalid (I) :缓存中的数据无效,不能使用。
工作机制:MESI 通过监控各个核心之间的数据状态(嗅探机制),保证每当一个核心修改了数据,其他核心的缓存会自动将该数据标记为无效,从而保证一致性。
这里说明一下,MESI是一种协议,所以它会有各种实现,甚至嗅探机制也有各种实现(存在延迟可能)
MESI的优化:
- 传统嗅探机制,要求各核心立即响应其他核心的读写操作,性能太低,此时CPU引入了Invalid Queue
Invalid Queue
缓存无效化操作:当 MESI 协议检测到某个核心修改了数据,它会通知其他核心无效化相关缓存行。
解决方案是把同步改为异步,增加Invalid Queue异步处理,可以批量或延迟处理无效化操作,减少总线带宽压力
带来的问题:主要问题是可能导致的延迟同步
Store Buffer
为了提高写操作效率,CPU 引入了 Store Buffer,允许写操作先暂存到缓冲区,而不是直接写入缓存或内存。直到缓存满了、空闲等时候真正刷到内存(常规套路了)
带来的问题:由于 Store Buffer 的存在,当一个核心修改数据并将其暂存到 Store Buffer 中时,MESI 可能还未检测到这一变化。这就带来了可见性问题
Invalid Queue和store buffer的使用类似于消息队列,导致的问题是MESI更加容易出现数据不一致
如何解决Invalid Queue和store buffer造成的问题?
为了解决这两个带来的可见性问题,处理器设计了 内存屏障给程序员使用。
内存屏障
写屏障 (Store Barrier) :确保写操作真正写回到内存,清空 Store Buffer。保证其他核心读取到最新数据。
读屏障 (Load Barrier) :确保在屏障前的读操作完成,确保 Invalid Queue 中的无效化操作已处理。
全屏障 (Full Barrier) :结合读写屏障的功能,确保读写操作的完整顺序,清空 Store Buffer 并处理 Invalid Queue,确保多核环境中的数据一致性和可见性。
通过这些机制,内存屏障在多核处理器中协调 Store Buffer 和 Invalid Queue,解决了由于指令重排、缓存滞留等带来的数据不一致问题。
高级语言如何使用内存屏障
Java
-
volatile
关键字在 Java 中,
volatile
关键字用于声明一个共享变量写入
volatile
变量:会在写入时插入一个 写屏障,确保之前对普通变量的写操作都已经完成并刷新到内存。读取
volatile
变量:会在读取时插入一个 读屏障,确保之后的读操作读取到的是主内存中的最新值。 -
synchronized
关键字synchronized
块或方法会确保进入块或者方法之前,其他线程对共享变量的修改对当前线程可见,这相当于在进入和退出时分别插入了内存屏障。