std::call_once用于确保多线程下某函数仅执行一次,需配合std::once_flag使用,后者须为静态或全局对象;若初始化函数抛异常,仅首个线程抛出,其余线程等待后直接返回。

std::call_once 用来干啥
它就是 C++11 引入的线程安全一次性初始化工具,核心作用是:**确保某个函数(或 lambda)在多线程环境下只被执行一次,哪怕被多个线程同时调用 std::call_once**。
典型场景比如单例的懒初始化、全局资源首次加载、配置解析等——你不能靠 if (flag) { init(); flag = true; },那会竞态。
必须配 std::once_flag,不能单独用
std::call_once 不是独立函数,它必须和一个 std::once_flag 对象绑定使用。这个 flag 是状态载体,内部用原子操作维护“是否已执行”,所以不能传值、不能拷贝、不能局部静态(除非你明确知道生命周期)。
常见错误:
立即学习“C++免费学习笔记(深入)”;
- 把
std::once_flag声明成局部变量 → 每次调用都新建一个,完全失效 - 用
std::once_flag flag{};在函数内定义 → 同上,毫无意义 - 试图
std::move或拷贝std::once_flag→ 编译失败,它删了拷贝/移动构造
正确姿势是:静态存储期(static)、全局、或类静态成员。
异常发生时,std::call_once 仍保证“只执行一次”
如果传给 std::call_once 的函数抛出异常,该次调用视为“未成功”,flag 不置位;下一次调用仍会尝试执行——但注意:**只有第一个抛异常的线程会把异常往外扔,其余阻塞线程会等它结束,然后直接返回(不重试)**。
这意味着:
- 别指望靠反复调用
std::call_once来重试失败初始化 - 若初始化逻辑可能失败,应在函数内部处理异常,或用额外标志位区分“失败过”和“未执行”
- 不要在初始化函数里做不可回滚的操作(比如部分写文件),因为无法预知哪个线程先跑、哪个先崩
示例:
static std::once_flag init_flag;
static SomeResource* resource = nullptr;
<p>void do_init() {
resource = new SomeResource(); // 可能抛 bad_alloc
}</p><p>// 安全调用:
std::call_once(init_flag, do_init);
性能和兼容性要注意什么
std::call_once 底层依赖平台的线程原语(如 pthread_once 或 Windows InitOnceExecuteOnce),首次调用有轻微开销(需原子检查+可能锁竞争),之后几乎零成本(纯原子读)。
兼容性方面:
- C++11 起支持,但早期 GCC(
- 嵌入式或 freestanding 环境可能没实现,得查标准库文档
- 别把它当锁用——它不保护后续访问,只是保“初始化动作”一次。resource 初始化完,还得自己加锁或用原子指针控制并发读写
最常被忽略的一点:很多人以为 std::call_once 能替代双重检查锁定(DCLP),但它不提供内存序保证(比如对初始化对象的写操作可能被重排)。实际中建议搭配 std::atomic<t></t> 或 std::shared_ptr 使用,否则可能看到未完全构造的对象。










