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
相关产品推荐
相关产品推荐

