Clang为何对部分全局变量采用线程安全初始化,对其他则不采用?
C++17 inline命名空间作用域全局变量的初始化实现
考虑使用C++17新增的inline变量特性声明的全局(命名空间作用域)变量,示例代码如下:
struct something { something(); ~something(); }; inline something global;
在x86平台的Clang 14环境中,启动阶段初始化该变量生成的汇编代码如下:
__cxx_global_var_init: # @__cxx_global_var_init push rbx mov al, byte ptr [rip + guard variable for global] test al, al je .LBB0_1 .LBB0_4: pop rbx ret .LBB0_1: mov edi, offset guard variable for global call __cxa_guard_acquire test eax, eax je .LBB0_4 mov edi, offset global call something::something() [complete object constructor] mov edi, offset something::~something() [complete object destructor] mov esi, offset global mov edx, offset __dso_handle call __cxa_atexit mov edi, offset guard variable for global pop rbx jmp __cxa_guard_release # TAILCALL mov rbx, rax mov edi, offset guard variable for global call __cxa_guard_abort mov rdi, rbx call _Unwind_Resume@PLT global: .zero 1 guard variable for global: .quad 0 # 0x0
这是双重检查锁定模式,可实现线程安全的初始化流程:
- 第一条
test al, al指令先执行乐观的初始无锁检查,判断变量是否已完成初始化 - 如果检查结果显示变量未初始化,则调用
__cxa_guard_acquire,该函数会在锁保护下再次检查同一保护变量,避免两个及以上线程同时通过初始检查的竞态问题,最终仅有一个线程能通过第二次检查完成初始化。
该模式与初始化非平凡类型的函数局部静态变量所用的模式完全一致,C++标准明确要求这类局部静态变量采用线程安全的延迟初始化。
C++17前模板静态持有者模式的初始化实现
C++17之前,头文件中实现全局变量常用“模板静态持有者”模式,代码示例如下:
struct something { something(); ~something(); }; template <typename T = void> struct holder { static something global; }; template <typename T> something holder<T>::global; void instantiate() { (void)holder<void>::global; }
此处的holder类用于支持holder<T>::global在多个翻译单元中实例化并保证行为正确(即交由链接器处理合并为唯一实体),这与命名空间作用域全局变量、非模板类静态变量的单一定义规则场景不同。instantiate()调用的作用仅是实际触发模板及关联静态成员的实例化,否则编译器不会生成任何相关代码。
对应生成的汇编代码如下:
instantiate(): # @instantiate() ret __cxx_global_var_init: # @__cxx_global_var_init push rax cmp byte ptr [rip + guard variable for holder<void>::global], 0 je .LBB1_1 pop rax ret .LBB1_1: mov edi, offset holder<void>::global call something::something() [complete object constructor] mov edi, offset something::~something() [complete object destructor] mov esi, offset holder<void>::global mov edx, offset __dso_handle call __cxa_atexit mov byte ptr [rip + guard variable for holder<void>::global], 1 pop rax ret holder<void>::global: .zero 1 guard variable for holder<void>::global: .quad 0 # 0x0
可以看到双重检查锁定逻辑完全消失,代码仅对保护变量做了一次无锁检查。
核心疑问
为何会存在这种实现差异?这仅仅是编译器实现层面的特殊处理,还是源自C++标准的相关要求?
通常来看这类全局构造函数似乎不需要加锁,因为这些生成的初始化函数一般在main函数执行前的程序启动阶段、或动态加载共享对象时的单线程上下文中被调用。但也存在未被覆盖的场景可能性,例如并行加载两个引用同一全局变量的共享对象时是否会触发竞态?
内容的提问来源于stack exchange,提问作者BeeOnRope
相关产品推荐
相关产品推荐

