易失性与内存屏障

Mar*_*chs 0 c multithreading gcc lock-free memory-barriers

是否有可能通过内存屏障实现易失性变量的相同“保证”(始终读/写内存而不是寄存器)?只需在一个线程中写入变量,然后在另一个线程中读取其值。下面的内容是等价的吗?

#define rmb() __sync_synchronize()
#define wmb() __sync_synchronize()
static volatile int a;
static int b;

//invoked by thread 1
static void writer(void)
{
    ...
    a = 1;
    ...
    b = 1;
    wmb();
}

//invoked by thread 2
static void reader(void)
{
    ...
    while (a != 1)
        ;
    // do something
    ...
    while (b != 1)
        rmb();
    // do something
}
Run Code Online (Sandbox Code Playgroud)

编辑:好的,我知道 volatile 不能保证原子性、可见性或顺序。内存屏障除了排序之外还提供其他功能吗?还有能见度?除了 _Atomic C11 或 gcc/clang 原子内置函数之外,还有其他东西可以保证可见性吗?

Pet*_*des 5

不,仅靠障碍是不够的。volatile如果您想像 Linux 内核那样滚动自己的原子,依赖于一些已知编译器的行为(而不是像 那样的可移植 ISO C 东西) ,您也需要使用来访问共享数据<stdatomic.h>

在 GNU C 中,像编译器内存屏障这样的编译器内存屏障asm("" ::: "memory")可以强制编译器制作volatile int再次访问 non- 的汇编语言,而不是仅仅将其值保留在寄存器中。例如,您可能认为可以将其放入while (b != 1) asm(""::"memory")旋转循环中以强制重新读取b,这与损坏的代码不同,它会像这样进行优化 if(b!=1){ while(true){} }

但这并不是您唯一需要担心的事情:如果没有volatile,编译器就可以发明读取。例如,如果您这样做int tmp = shared_var;然后tmp多次使用,编译器可能会认为重新加载shared_var以供以后使用之一会更便宜。所以你的程序可能会表现得像tmp改变了的值,导致不一致的行为。

请参阅 LWN 文章谁害怕一个糟糕的优化编译器?对于这个以及更多可能的问题;它解释了为什么 Linux 代码需要WRITE_ONCE/READ_ONCEACCESS_ONCE宏来访问volatile共享变量。如果您正在滚动自己的版本,则不需要声明变量本身volatile;就足够了*(volatile int*)&b = 1;。(这使您可以在程序的不共享阶段有效地访问它。或者对于结构体,当该结构体的实例不共享时。)

GCC 和 clang 至少在事实上定义了多线程用例的行为volatile(类似于_Atomicwith memory_order_relaxed),以支持 Linux 内核和在 C++11 / C11 stdatomic 为我们提供可移植性之前编写的遗留代码定义了完成所有这些事情的方法。通常你应该只使用stdatomic.hC 中的函数,或者GNU C__atomic内置函数。(或者__sync如果你坚持的话,可以使用过时的内置函数)。但volatile如果您确切地知道自己在做什么并且一切都正确的话,它仍然适用于许多已知的实现,例如 GCC、clang、ICC 和 MSVC。

volatile如果可以的话,使 GCC 使用单个全角访问进行访问,例如,对于 AArch64 上非原子示例stpint64_t,其中高半 = 低半,GCC 在一个寄存器中生成完整的 64 位值,如果您使用volatile,这样当该宽度的类型可以免费发生时,您就可以获得原子性。(不过,它不会特意在 32 位目标上执行 64 位原子性,__atomic_load_n这与使用 SSE2 或 MMX 在 32 位 x86 上执行 64 位加载不同。)

ISO C 不保证任何有关volatile;data-race UB 仍然适用于它。但是,我们在多个核心上运行同一进程的多个线程的所有现实世界系统在这些核心之间都具有缓存一致的共享内存,因此volatile强制在 asm 中进行加载或存储确实提供了可见性。但没有运行时排序。


顺便说一句,__sync_synchronize对于读内存屏障或写内存屏障(不需要阻止 StoreLoad 重新排序的获取/释放栅栏)来说,这是一个非常昂贵的定义。 __sync_synchronize实际上是一个完整的屏障,就像atomic_thread_fence(memory_order_seq_cst);