You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 13:48:19