mfence, lfence, sfence什么作用?

本文深入解析内存屏障的概念,包括mfence, lfence, sfence的作用及其在多处理器环境下的必要性。探讨了它们如何防止指令重排序,确保数据一致性,以及在Java中如何通过synchronized关键字和volatile变量体现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

mfence, lfence, sfence什么作用?

// src/backend/utils/rac/lock_free_queu.array_spsc_queue.c
#define mb() 	asm volatile("mfence":::"memory")
#define rmb()	asm volatile("lfence":::"memory")
#define wmb()	asm volatile("sfence" ::: "memory")

就是保证内存访问的串行化,内部操作就是在一系列内存访问中添加若干延迟,保证此指令之后的内存访问发生在此指令之前的内存访问完成之后(不出现重叠)。
lfence 读串行化
sfence 写串行化
mfence 读写都串行化

参考

内存屏障

为什么需要内存屏障?

由于现代的操作系统都是多处理器.而每一个处理器都有自己的缓存,并且这些缓存并不是实时都与内存发生信息交换.这样就可能出现一个cpu上的缓存数据与另一个cpu上的缓存数据不一致的问题.而这样在多线程开发中,就有可能导致出现一些异常行为. 而操作系统底层为了这些问题,提供了一些内存屏障用以解决这样的问题

内存屏障的作用
  1. 阻止屏障两边的指令重排序

2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效(意思就是确保读到的数据都是内存中的最新的数据,确保数据的有效性)

内存屏障的分类

硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力。拿X86平台来说,有几种主要的内存屏障

1. lfence,是一种Load Barrier 读屏障。在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据

2. sfence, 是一种Store Barrier 写屏障。在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存

3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力

4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

Lock前缀实现了类似的能力.

1. 它先对总线/缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的脏数据全部刷新回主内存。

2. 在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。Lock后的写操作会让其他CPU相关的cache line失效,从而从新从内存加载最新的数据。这个是通过缓存一致性协议做的。

内存屏障使用例子

LoadLoad屏障(读读屏障):对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障(写写屏障):对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障(读写屏障):对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障(写读屏障):对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

java中内存屏障的常见例子

通过 Synchronized关键字包住的代码区域,当线程进入到该区域读取变量信息时,保证读到的是最新的值.这是因为在同步区内对变量的写入操作,在离开同步区时就将当前线程内的数据刷新到内存中,而对数据的读取也不能从缓存读取,只能从内存中读取,保证了数据的读有效性.这就是插入了StoreStore屏障
使用了volatile修饰变量,则对变量的写操作,会插入StoreLoad屏障.
其余的操作,则需要通过Unsafe这个类来执行.

内存屏障技术是并发编程中确保内存操作顺序性的关键,它们能够阻止编译器或CPU对指令进行重排序。在编写并发程序时,为了保证内存操作的可见性和一致性,程序员需要理解并正确使用内存屏障。 参考资源链接:[CPU缓存与内存顺序:并发编程理解](https://wenku.csdn.net/doc/61wzys5hp0?spm=1055.2569.3001.10343) 在C++11标准中,内存屏障被分为两类:编译器内存屏障(Compiler Memory Barrier)和CPU内存屏障(CPU Memory Barrier)。编译器内存屏障通过编译器指令实现,如`std::atomic_thread_fence`,可以防止编译器对相关指令进行重排序。CPU内存屏障则通过特定的CPU指令实现,例如x86架构中的`lfence`、`mfence`和`sfence`指令。 为了说明如何使用内存屏障,我们考虑以下场景:两个线程分别执行读写操作,读线程希望在写入操作完成后才开始读取,以避免读取到过期的数据。这里我们可以使用`std::atomic_thread_fence`来建立内存屏障: ```cpp #include <atomic> #include <thread> std::atomic<int> flag = {0}; int shared_data = 0; void write_thread() { // 执行某些操作... shared_data = 42; // 写入数据 // 在写入操作和设置flag之间插入store-release内存屏障 std::atomic_thread_fence(std::memory_order_release); flag.store(1, std::memory_order_release); // 设置flag } void read_thread() { int expected = 1; // 在读取数据和检查flag之间插入load-acquire内存屏障 while (!flag.load(std::memory_order_acquire)) { std::atomic_thread_fence(std::memory_order_acquire); } // 读取操作 int data = shared_data; // 确保读取到的是最新的数据 } int main() { std::thread writer(write_thread); std::thread reader(read_thread); writer.join(); reader.join(); return 0; } ``` 在上述示例中,`std::atomic_thread_fence`被用来创建内存屏障。写线程在设置`flag`之前插入了`std::memory_order_release`内存屏障,而读线程在检查`flag`之前插入了`std::memory_order_acquire`内存屏障。这样可以确保写操作在`flag`被设置之前完成,并且当`flag`被读取时,相应的写操作的结果对读线程可见。 通过这种方式,内存屏障技术在并发编程中起到了至关重要的作用,它允许开发者精确控制内存操作的执行顺序,确保程序的正确性和高效性。如果你希望进一步深入了解CPU缓存、内存排序和并发程序设计,强烈推荐查阅《CPU缓存与内存顺序:并发编程理解》。这份资料不仅涵盖了内存屏障的使用,还包括了多核处理器的缓存一致性协议,以及并发编程中其它高级主题,如spinlock和False Sharing,提供了一站式的深入学习体验。 参考资源链接:[CPU缓存与内存顺序:并发编程理解](https://wenku.csdn.net/doc/61wzys5hp0?spm=1055.2569.3001.10343)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值