C++中"内存屏障"的示例

12

我在阅读与volatile关键字相关的问题的答案:

https://dev59.com/5HE95IYBdhLWcg3wDpYG#2485177

这个人说:
解决防止重新排序的方法是使用内存屏障,它向编译器和CPU指示不能在此点之间重新排序任何内存访问。在我们的易失变量访问周围放置这样的屏障确保即使非易失性访问也不会在易失性访问之间重新排序,从而允许我们编写线程安全的代码。
然而,内存屏障还确保在达到屏障时执行所有待处理的读/写操作,因此它本身有效地为我们提供了所需的一切,使易失性变量变得不必要。我们可以完全删除易失性限定符。
那么,在C++中如何实现这个“内存屏障”呢?
编辑:
有人能给一个简单的代码示例吗?

@HansPassant 在你所提供的问题中,没有一个简单的C++内存屏障示例。 - user997112
谁说这会很简单?这是C++,它应该很难。如果不是这样的话,那么任何人都可以成为C++程序员 :) 至少问题标题中的“内存屏障”一词应该暗示这是完全相同的问题。 - Hans Passant
3个回答

12

C++11中使用内存屏障很简单:

std::atomic<int> i;

所有对 i 的访问都将受到内存屏障的保护。


实际上,我仍然推荐这个视频https://youtu.be/ZQFzMfHIxng ;)。 - jaques-sam
除非您特别不关心访问与其他对象上的操作的顺序,否则您可以使用i.load(std::memory_order_relaxed)。 (默认值为seq_cst,但通常仅需要使用acquire和release,并且可能更便宜,尤其是在x86上存储时) - Peter Cordes

8

这非常依赖于硬件。根据Linux内核中相当长的内存屏障文档

The Linux kernel has eight basic CPU memory barriers:

TYPE                MANDATORY               SMP CONDITIONAL
===============     ======================= ===========================
GENERAL             mb()                    smp_mb()    
WRITE               wmb()                   smp_wmb()
READ                rmb()                   smp_rmb()   
DATA DEPENDENCY     read_barrier_depends()  smp_read_barrier_depends()

让我们特别看看其中一个: smp_mb()。 如果你打开 asm/x86/um/asm/barrier.h,你会发现当CONFIG_SMP被定义时,

#define smp_mb()    mb()

如果您向上滚动,您会看到根据平台的不同,mb具有不同的实现方式:

// on x86-32
#define mb()        alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
// on other platforms
#define mb()        asm volatile("mfence" : : : "memory")

这两个东西的区别更多信息在这篇帖子中讨论。希望这能帮到你。


这些是系统调用吗?所以程序员不能自己创建内存屏障? - user997112
5
它们不是系统调用(顺便说一下,您可以通过运行“man 2 syscalls”来查找所有系统调用)。C++编译器将在已编译的代码中用相应的汇编指令替换您的mb()调用。你可以自己创建一个内存屏障,这将包括调用一些汇编指令,就像Linux源代码所做的那样。 - qdii

2
通常,有“内在函数”-这些是编译器具有特殊知识的特殊函数,以了解它们的操作方式(特别是它们是内存屏障)。名称因编译器而异(有时还因同一编译器的不同架构而异)。
例如,MSVC使用_ReadBarrierWriteBarrier_ReadWriteBarrier
在x86中,它将生成lfencesfencemfence指令-分别对应于“load”、“store”和“all memory operations”的屏障-换句话说,lfence将是内存读取操作的屏障,sfence将是“内存写入”屏障,mfence将是针对读取和写入操作的屏障。

这实际上并不是 lfence 的作用;它唯一的内存排序效果是阻止 SSE4.1 movntdqa 从弱序 WC 内存(例如视频 RAM)重新排序加载,与后续的加载/存储重排。 它还阻止 OoO 执行后续指令,直到所有更早的指令都完成执行(但是对于存储,不一定要从存储缓冲区提交)。 x86 已经禁止了除 StoreLoad 以外的普通加载/存储内存重排序,因此编译器可以通过不进行编译时重排序来获得 LoadLoad 和 LoadStore 排序。 - Peter Cordes
MSDN的链接已经失效,但是这些内部函数并不会发出那些指令;它们似乎只会阻止编译时的重新排序。一些内部函数会让编译器不进行常量传播,比如GNU C的asm("" ::: "memory"),所以它实际上会重新加载。https://godbolt.org/z/Gneborsxf 在MSVC 19.28 -O2(x86或x64)中甚至没有显示任何汇编代码,包括_ReadWriteBarrier。另请参见When should I use _mm_sfence _mm_lfence and _mm_mfence了解实际的内部函数和为什么通常只需要使用编译器障碍。 - Peter Cordes
1
@PeterCordes 是正确的。_Read[Write]Barrier 内置函数是编译器屏障,而不是内存屏障。Win32 内置函数用于内存屏障是 MemoryBarrier。文档(https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-memorybarrier)对前面的函数提供了澄清:- _ReadBarrier_WriteBarrier_ReadWriteBarrier 编译器内置函数仅防止编译器重新排序。 - Mark Ingram
@MarkIngram - 什么是Linux x64上的内在/系统内存屏障? - Boppity Bop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接