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

C++多线程初始化:atomic_flag实现是否存在竞态风险

一次性初始化逻辑的线程安全问题分析

标准库推荐实现

在《C++ Concurrency in Action(第二版)》3.3.1节中提到,多线程场景下执行一次性初始化操作,推荐使用std::call_once规避双重检查锁定模式的固有缺陷,示例代码如下:

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;

void init_resource()
{
    resource_ptr.reset(new some_resource);
}
void foo()
{
    std::call_once(resource_flag,init_resource);
    resource_ptr->do_something();
}

std::call_once内部封装了完整的内存屏障、线程同步逻辑,能严格保证三个核心语义:

  • 初始化函数仅会被唯一一个线程执行一次
  • 初始化完成前,所有调用std::call_once的线程都会阻塞等待
  • 初始化完成后,所有线程都能正确观测到初始化操作写入的全部内存状态,不存在指令重排、内存可见性导致的未定义行为

基于atomic_flag自实现方案的缺陷

此前常用的atomic_flag初始化实现属于非线程安全写法,存在明确的竞态风险,原代码修正拼写错误(原代码漏写std::命名空间前缀)后如下:

std::atomic_flag init = ATOMIC_FLAG_INIT;
std::atomic<bool> initialized = false;
void Init()
{
    if (init.test_and_set()) return;
    DoInit();
    initialized = true;
}
void Foo(){
    if(!initialized) return;
    DoSomething(); // 访问DoInit()中初始化的变量
}

哪怕约定所有线程调用Foo()前一定会先调用Init(),这段代码依然存在三个无法绕过的问题:

  • 指令重排风险真实存在:如果编译器通过过程间分析确定DoInit()内部不会访问initialized变量,在不改变单线程可观测行为的前提下,完全可能将initialized = true的写入指令调整到DoInit()调用之前,直接出现资源未初始化就标记完成的问题。在ARM、RISC-V等弱内存序架构下,如果没有正确配置内存屏障,硬件层面的乱序执行也可能导致其他线程先看到初始化完成标记、后看到资源的实际写入结果。
  • 逻辑语义存在硬伤:第一个线程执行DoInit()的过程中,其他线程调用Init()会因为init.test_and_set()返回true直接跳过等待,进入Foo()时因为initialized还是false直接返回,这部分业务调用会直接丢失,完全做不到“所有调用都等待初始化完成后再执行业务逻辑”的基本要求。如果DoInit()执行过程中抛出异常,init标记已经被永久置位,后续所有线程都无法再触发初始化,整个逻辑会永久失效。
  • 内存可见性无可靠保障:如果没有为原子操作手动配置正确的acquire/release内存序配对,部分架构下其他线程读到initialized为true时,依然可能无法观测到DoInit()写入的全部资源状态,访问到未初始化的脏数据,触发未定义行为。

日常开发中不推荐手动用原子变量实现一次性初始化逻辑:如果一定要自实现,必须手动给原子操作搭配正确的内存序建立happens-before关系,同时增加线程等待逻辑、异常回退逻辑,开发成本和出错概率极高,直接使用标准库的std::call_once是最稳妥的选择。

内容的提问来源于stack exchange,提问作者codesavesworld

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 13:57:19