std::call_once通过std::once_flag实现线程安全单次执行,依赖原子操作与系统原语确保有且仅一个线程执行函数,其余等待;flag必须静态/全局生命周期、不可复制移动,且首次调用有开销,后续近乎零成本。

call_once 是靠 std::once_flag 实现线程安全的单次执行
call_once 本身不保存状态,它必须和一个 std::once_flag 对象配对使用。这个 flag 内部用原子操作 + 操作系统原语(如 futex 或 CRITICAL_SECTION)实现状态切换。第一次调用 call_once(flag, func) 时,会将 flag 标记为“已触发”,后续所有线程再调用都会直接返回,不执行 func。
关键点在于:同一个 std::once_flag 实例只能用于一次初始化逻辑;不同 flag 之间互不影响。
多个线程同时调用 call_once 会阻塞等待,而非竞态失败
这不是“谁抢到谁执行”的竞争模型,而是协作式同步:所有并发调用 call_once 的线程中,有且仅有一个会真正执行传入的函数,其余线程会挂起并等待该函数完成,然后全部继续往下走。
常见误判场景:
立即学习“C++免费学习笔记(深入)”;
- 以为“没报错就说明执行了”——其实可能你所在的线程根本没执行
func,只是等别人执行完就醒了 - 在
func里抛异常:整个call_once调用仍算“已完成”,flag 被标记为已触发,但后续调用会直接 rethrow 同一个异常(C++11 起标准行为) - 把
std::once_flag放在栈上或作为临时对象——它必须是静态或全局生命周期,否则未定义行为
std::once_flag 不能复制、不能移动,必须显式定义
std::once_flag 是 trivially destructible 但 non-copyable/non-movable 的类型。你不能写 auto flag = std::once_flag{};,也不能把它塞进 std::vector 或当作函数参数传值。
正确用法只有三种:
- 静态局部变量:
static std::once_flag flag;(最常用) - 全局/命名空间作用域变量
- 类的 static 成员变量
错误示例:void foo() { std::once_flag flag; call_once(flag, init); } —— 每次调用都新建 flag,完全失去“只一次”的意义。
和 pthread_once、std::call_once 的 ABI 兼容性有关联但不等价
在 Linux 上,libstdc++ 和 libc++ 都基于 pthread_once 实现 std::call_once;Windows 上则用 SRWLock 或 InitOnceExecuteOnce。这意味着:
- 跨动态库边界共享同一个
std::once_flag要小心:如果两个 .so 分别链接不同版本 libstdc++,flag 可能无法正确识别彼此状态 - 不要混用
pthread_once和std::call_once操作同一个初始化逻辑——它们的 flag 类型不兼容 - 性能上,首次调用有明显开销(需进入内核),之后是纯原子读,几乎无成本
真正容易被忽略的是:std::once_flag 的内存布局不是标准规定的,所以不能用 memset 初始化,也不能依赖其字节值做判断——必须用默认构造。


















