
std::call_once 用来保证某个函数在多线程下仅执行一次
它本身不执行函数,而是配合 std::once_flag 控制执行时机:多个线程同时调用 std::call_once(flag, func),最终只有其中一个线程会真正运行 func,其余线程会阻塞直到 func 完成,然后全部继续向下执行。
必须搭配 std::once_flag 使用,且 once_flag 必须是静态或全局生命周期
如果 std::once_flag 是局部变量(比如函数内定义),每次调用函数都会新建一个 flag,就完全失去“只执行一次”的意义;更严重的是,它的内部状态依赖静态存储期,局部对象可能引发未定义行为。
- ✅ 正确:声明为
static std::once_flag flag;或全局/类静态成员 - ❌ 错误:
std::once_flag flag;(栈上临时对象) - ⚠️ 注意:
std::once_flag不可拷贝、不可移动,也不能手动赋值
传入的函数不能抛异常,否则程序直接 terminate
std::call_once 内部实现要求被调函数必须 noexcept。如果 func 抛出异常,C++ 标准规定调用 std::terminate() —— 不是捕获、不是重试,而是直接结束进程。
- 解决办法:在 lambda 或包装函数里用
try/catch吞掉异常,或确保逻辑无异常路径 - 常见翻车点:在初始化中调用可能抛异常的
std::thread构造、new失败、文件打开失败等 - 示例安全写法:
static std::once_flag flag;
std::call_once(flag, []() noexcept {
try {
// 可能出错的初始化逻辑
init_resource();
} catch (...) {
// 记录日志或设置错误状态,但绝不让异常逃出
g_init_failed = true;
}
});
和 std::mutex + 手动标志相比,它更轻量且避免竞态
手动实现“只执行一次”容易写出竞态:比如先判断 if (!inited) { inited = true; do_init(); },其中赋值和调用之间存在窗口,两个线程都可能进入。而 std::call_once 是原子性地检查+标记+执行,由标准库在底层用平台原语(如 futex、SRWLock)高效实现。
立即学习“C++免费学习笔记(深入)”;
- 性能上:首次调用有同步开销,后续调用几乎无成本(通常只是读一个内存位置)
- 兼容性:C++11 起支持,所有主流 STL 实现(libstdc++、libc++、MSVC STL)都正确实现
- 典型场景:单例构造、日志系统初始化、OpenSSL 初始化、GPU 上下文首次绑定
std::once_flag 的生命周期约束和异常安全性——这两点一旦出错,问题往往延迟暴露,调试成本远高于加锁逻辑本身。










