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

基于自旋锁的单次调用实现:性能与内存序优化问询

多线程单次初始化机制的实现优化与内存序分析

需求背景

要实现一种多线程安全的单次初始化机制,确保初始化逻辑即使被多线程反复调用也仅执行一次,本质是用GCC原子操作和自旋锁模拟pthread_once的功能,目标架构覆盖x86_64和aarch64。预期API使用示例如下:

void gets_called_many_times_from_many_threads(void)
{  
    static int my_once_flag = 0; 
     
    if (once_enter(&my_once_flag)) {
        // 仅执行一次的初始化逻辑
        once_commit(&my_once_flag);
    }

    // 依赖初始化完成的后续逻辑
}

初始实现与问题

初始实现代码如下:

int once_enter(int *b)
{
    int zero = 0;
    int got_lock = __atomic_compare_exchange_n(b, &zero, 1, 0, __ATOMIC_RELAXED, __ATOMIC_RELAXED);
    if (got_lock) return 1;

    while (2 != __atomic_load_n(b, __ATOMIC_ACQUIRE)) {
        // x86平台可在此插入pause指令
    };
    return 0;
}

void once_commit(int *b)
{
    (void) __atomic_store_n(b, 2, __ATOMIC_RELEASE);
}

存在的疑问与问题:

  • 对CAS操作使用__ATOMIC_RELAXED内存序的合理性存疑,担心无法保证初始化逻辑与后续代码的内存可见性。
  • x86平台下lock cmpxchg是全内存屏障,在已完成初始化的常见场景下,每次调用once_enter都会触发这个屏障,带来不必要的性能开销。

更新后的实现与设计思路

为优化常见场景的性能,更新实现采用了双重检查锁模式,并统一使用__ATOMIC_SEQ_CST内存序,代码如下:

#ifdef __x86_64__
#define PAUSE()  __asm __volatile("pause")
#else
#define PAUSE()
#endif

int once_enter(int *b)
{
    if(2 == __atomic_load_n(b, __ATOMIC_SEQ_CST)) return 0;

    int zero = 0;
    int got_lock = __atomic_compare_exchange_n(b, &zero, 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST);
    if (got_lock) return 1;

    while (2 != __atomic_load_n(b, __ATOMIC_SEQ_CST)) {
        PAUSE();
    };
    return 0;
}

void once_commit(int *b)
{
    (void) __atomic_store_n(b, 2, __ATOMIC_SEQ_CST);
}

更新的原因:

  • 优先保障已初始化场景的性能:首次读取标记位,若已完成初始化(值为2)则直接返回,避免进入CAS逻辑。
  • 观察到GCC在x86上对__ATOMIC_ACQUIRE的读取不会生成内存栅栏指令,但为了简化跨平台逻辑,直接使用__ATOMIC_SEQ_CST。

实现中的问题分析

  1. 内存序冗余,性能浪费

    • 在已初始化场景下,x86平台的__ATOMIC_SEQ_CST读取会生成lock orl $0,(%rsp)这类全屏障指令,而实际上仅需要__ATOMIC_ACQUIRE(x86上无需额外屏障,仅保证读取的可见性)就能满足需求,SEQ_CST的全屏障会带来不必要的性能开销。
    • aarch64平台上SEQ_CST的开销更大,远超过ACQUIRE/RELEASE的组合。
  2. CAS操作的内存序过度严格

    • CAS操作将状态从0改为1时,仅需要保证这个修改对其他线程可见,使用__ATOMIC_RELEASE作为成功时的内存序即可,失败时用__ATOMIC_RELAXED完全足够,无需SEQ_CST。
  3. 自旋等待的内存序优化空间

    • 自旋等待时的读取使用__ATOMIC_ACQUIRE即可,无需SEQ_CST,因为只需要保证读到状态2时,初始化逻辑的所有修改都已可见。
  4. 初始实现的CAS内存序隐藏风险

    • 初始实现中CAS用RELAXED确实存在隐患:当线程A成功CAS并进入初始化逻辑,线程B在自旋等待时用ACQUIRE读取状态,虽然commit时用RELEASE能保证初始化逻辑与状态2的同步,但CAS将0改为1的操作是RELAXED,无法保证其他线程看到状态1时的内存可见性(不过因为后续自旋等待的是状态2,这个风险在当前逻辑下被掩盖,但并非正确的内存序设计)。

优化建议的实现版本

兼顾性能与正确性的版本应该如下,针对不同场景使用最小必要的内存序:

#ifdef __x86_64__
#define PAUSE()  __asm __volatile("pause")
#else
#define PAUSE()
#endif

int once_enter(int *b)
{
    // 已初始化场景:用ACQUIRE保证读取到2时,初始化逻辑的修改都可见
    if(2 == __atomic_load_n(b, __ATOMIC_ACQUIRE)) return 0;

    int zero = 0;
    // CAS成功时用RELEASE:保证初始化逻辑的修改能被后续读取状态2的线程看到
    // CAS失败时用RELAXED:不需要同步任何操作
    int got_lock = __atomic_compare_exchange_n(b, &zero, 1, 0, __ATOMIC_RELEASE, __ATOMIC_RELAXED);
    if (got_lock) return 1;

    // 自旋等待:用ACQUIRE保证读到2时,初始化逻辑完成
    while (2 != __atomic_load_n(b, __ATOMIC_ACQUIRE)) {
        PAUSE();
    };
    return 0;
}

void once_commit(int *b)
{
    // RELEASE内存序:保证初始化逻辑的所有写操作在状态2被写入前完成,且对其他线程可见
    (void) __atomic_store_n(b, 2, __ATOMIC_RELEASE);
}

这个版本的优势:

  • 已初始化场景下,x86平台的__ATOMIC_ACQUIRE读取无额外屏障,性能最优;aarch64平台仅生成轻量的ldar指令。
  • CAS操作使用最小必要的内存序,避免不必要的屏障开销。
  • 内存序设计严谨,完全保证初始化逻辑与后续代码的内存可见性,跨平台兼容x86_64和aarch64。

内容的提问来源于stack exchange,提问作者Harris M Snyder

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 07:35:21