只有编译器的内存屏障(如std::atomic_signal_fence)何时有用

When is a compiler-only memory barrier (such as std::atomic_signal_fence) useful?

本文关键字:signal atomic fence 有用 何时 std 编译器 内存      更新时间:2023-10-16

编译器围栏的概念经常出现在我阅读有关内存模型、屏障、排序、原子学等的文章时,但通常情况下,它是在CPU围栏配对的上下文中出现的,正如人们所期望的那样。

然而,偶尔我会读到一些围栏结构,适用于编译器。这方面的一个例子是C++11 std::atomic_signal_fence函数,它在cppreference.com上声明:

std::atomic_signal_fence等效于std::atomic_thread_fence,除了没有CPU发布了用于存储器排序的指令。仅重新排序编译器的指令被抑制为顺序指令。

我有五个问题与这个主题有关:

  1. 正如名称std::atomic_signal_fence所暗示的,异步中断(例如线程被内核抢占以执行信号处理程序)是否是仅情况,其中仅编译器的围栏是有用的?

  2. 它的有用性是否适用于所有体系结构,包括强序体系结构,如x86

  3. 是否可以提供一个特定的示例来证明仅编译器围栏的有用性?

  4. 使用std::atomic_signal_fence时,使用acq_relseq_cst排序有什么区别吗?(我希望它不会有什么不同。)

  5. 第一个问题可能涵盖了这个问题,但我很好奇,无论如何都要具体问一下:是否有必要使用thread_local访问的围栏?(如果真的是这样的话,我希望只有编译器围栏(如atomic_signal_fence)才是的首选工具。)

谢谢。

要回答所有5个问题:


1) 编译器围栏(本身,没有CPU围栏)仅在两种情况下有用:

  • 在单个线程和绑定到同一线程(如信号处理程序)的异步中断处理程序之间强制执行内存顺序约束

  • 当保证每个线程将在同一CPU内核上执行时,在多个线程之间强制执行内存顺序约束。换句话说,应用程序将只在单核系统上运行,或者应用程序采取特殊措施(通过处理器亲和性)来确保共享数据的每个线程都绑定到同一个核。


2) 底层体系结构的内存模型,无论是强有序的还是弱有序的,都与在某种情况下是否需要编译器围栏无关。


3) 下面是伪代码,它演示了编译器围栏本身的使用,以充分同步线程和绑定到同一线程的异步信号处理程序之间的内存访问:

void async_signal_handler()
{
    if ( is_shared_data_initialized )
    {
        compiler_only_memory_barrier(memory_order::acquire);
        ... use shared_data ...
    }
}
void main()
{
// initialize shared_data ...
    shared_data->foo = ...
    shared_data->bar = ...
    shared_data->baz = ...
// shared_data is now fully initialized and ready to use
    compiler_only_memory_barrier(memory_order::release);
    is_shared_data_initialized = true;
}

重要注意:此示例假设async_signal_handler绑定到初始化shared_data并设置is_initialized标志的同一线程,这意味着应用程序是单线程的,或者它相应地设置线程信号掩码。否则,编译器围栏将不足,还需要CPU围栏


4) 它们应该是一样的acq_relseq_cst都应该产生一个完整的(双向)编译器围栏,不发出与围栏相关的CPU指令。只有当涉及多个核心和线程时,"顺序一致性"的概念才会发挥作用,而atomic_signal_fence只适用于一个执行线程。


5) 没有(当然,除非从异步信号处理程序访问线程本地数据,在这种情况下可能需要编译器围栏。)否则,线程本地数据永远不应该需要围栏,因为编译器(和CPU)只允许以不会从单线程角度改变程序相对于其序列点的可观察行为的方式对内存访问进行重新排序。从逻辑上讲,多线程程序中的线程局部静态与单线程程序中全局静态相同。在这两种情况下,数据只能从单个线程访问,这可以防止数据竞争的发生。

实际上有一些不可移植但有用的C编程习惯用法,其中编译器围栏是有用的,即使在多核代码中也是如此(尤其是在C11之前的代码中)。典型的情况是,程序正在进行一些通常会变得不稳定的访问(因为它们是对共享变量的),但您希望编译器能够移动访问。如果您知道访问在目标平台上是原子访问(并且您采取了一些其他预防措施),则可以使访问保持非易失性,但使用编译器屏障包含代码移动。

值得庆幸的是,大多数像这样的编程都被C11/C++11宽松的原子论所淘汰。