首页 > 解决方案 > 访问静态初始化变量时是否应该使用屏障?

问题描述

在我的函数中,我有以下两行代码:

static volatile uint64_t static_index = 0;
const uint64_t index = __sync_fetch_and_add(&static_index, 1, __ATOMIC_RELAXED);

如您所见,static_index在线程之间共享,而index在每个线程之间共享。我担心的是静态初始化可能会通过使用这个变量来重新排序,但我不确定这是否可以应用于静态(一次)初始化的变量。

__ATOMIC_RELAXED 是否足以避免在这种情况下重新排序?或者也许我应该使用__ATOMIC_RELEASE甚至__ATOMIC_SEQ_CST在这里?

我很感激任何帮助,谢谢。

标签: cmultithreadingatomicmemory-barriers

解决方案


您的静态初始化程序是一个编译时常量,因此您可以(至少在实践中)指望在0您的进程启动时已经拥有的静态存储空间。

(具体来说,它将在此处的 BSS 中。非零常数意味着它在该.data部分中。)


我很确定它对于非常量初始化程序也是安全的。

对于具有非常量初始化程序的函数范围静态变量,进入函数的第一个线程运行初始化程序。编译器通常使用保护变量。快速情况(已初始化)涉及对该保护变量的获取负载,以检查静态变量是否已被初始化。否则,原子 RMW 确保恰好有 1 个线程运行初始化程序,而其他线程则等待。

但是抛开实现细节:我没有仔细检查标准对静态变量的描述。但是在执行初始化的线程中,它static volatile foo = x显然在 RMW 之前排序,因此保证在之前发生。

在其他线程中,它们是否可以通过静态初始化重新排序成为问题。我认为这个问题的答案一定是否定的,否则你会在没有原子内置函数的情况下读取或写入数据竞争 UB。

在一个线程中,您可以将static foo = non_const;其视为确保foo已初始化。即使我们不是进行初始化的线程。

memory_order_release或者acquire如果其他线程与我们竞争,那么作为确保静态初始化在原子 RMW 之前完成的一种方式是没有意义的。从其他线程的 POV 中 控制我们操作的可见性顺序。我很确定语言规则只要求 RMW 发生在所有static foo = bar暗示的事情之后(无论是在执行初始化还是在必要时等待它),因为顺序排序。 如果您考虑非原子情况,其他任何事情都没有任何意义。您不能让其他线程读取未初始化的变量。

(请注意,C 仅支持函数范围的静态变量的非常量初始值设定项。只有 C++ 支持全局变量。)


顺便说一句,几乎没有理由使用已弃用/遗留的GNU C__sync内置函数:手册说:它们不应该用于应该使用“__atomic”内置函数的新代码。

内置函数的第三个参数__sync不是内存顺序,它是 GCC 忽略的“受内存屏障保护的变量的可选列表”。它__atomic_fetch_add采用内存顺序参数。


或者在大多数情况下更好,C11使用https://en.cppreference.com/w/c/atomic/atomic_fetch_add<stdatomic.h>对其进行_Atomic static uint64_t static_index = 0;修改

atomic_fetch_add_explicit(&static_index, 1, memory_order_relaxed);

(或者,如果您愿意,idx = static_index++;但默认为 seq_cst,因此对于非 x86 ISA 的编译效率会降低。)

您不需要volatile _Atomic,因此您可以删除volatile类型限定符。由于 C11 / C++11 可用,现在通常不建议使用volatile手动放松原子,但如果你这样做,那么简单的加载/存储访问有点像带有 mo_relaxed 的 _Atomic。volatile


推荐阅读