想象这样一个开发场景:你的程序同时运行着成百上千个线程,它们都需要访问同一个全局资源——可能是数据库连接池、日志系统,或者配置管理器。如何才能保证这个关键资源在整个程序生命周期中只被初始化一次,并且不会因为多线程的竞态条件而引发混乱?
传统的解决方案,例如双重检查锁定,实现起来不仅复杂,还容易因内存重排序等问题导致难以排查的Bug。而 C++11 标准为我们带来了一个既优雅又强大的工具组合:std::call_once 和它的搭档 std::once_flag。
std::call_once 是 C++11 标准库中提供的线程安全一次性执行机制。它的核心承诺是:无论有多少个线程、调用多少次,只要它们共享同一个 once_flag,那么关联的函数就保证只会被执行一次。
基本语法与核心特性
首先来看一下它的基础用法:
#include <mutex>
std::once_flag flag; // 控制标志,通常声明为静态或全局
std::call_once(flag, callable, args...); // callable只会执行一次
std::call_once 具备几个关键特性,使其成为并发初始化的首选:
- 严格一次性执行:绑定同一个
once_flag 的函数,无论被多少线程并发调用,都仅执行一次。
- 线程安全且无竞态:所有同步逻辑由标准库在底层实现,开发者无需手动管理锁,从根本上避免了竞争条件。
- 状态不可重置:
once_flag 的状态一旦被标记为“已完成”,就无法被修改或重置,后续所有调用都会直接跳过。
- 异常安全:如果被调用的函数抛出了未捕获的异常,
call_once 会视此次执行为失败,once_flag 的状态会被回滚,允许后续某个线程再次尝试执行。
底层原理:原子状态机
std::call_once 的行为可以通过一个原子状态机来理解。所有线程在调用时,都会先原子性地检查 once_flag 的内部状态,再根据状态决定接下来的动作。
典型的执行流程如下:
-
原子状态检查:
线程调用 call_once 时,首先通过无锁的原子指令读取 once_flag 的状态。
-
状态分支处理:
- 状态为“已完成”:直接返回,无任何同步开销(最快路径)。
- 状态为“执行中”:当前线程会被高效地挂起,等待执行线程完成工作并更新状态。
- 状态为“未执行”:当前线程尝试通过原子性的“比较并交换”操作,将状态从“未执行”修改为“执行中”。这个操作保证了只有一个线程能成功。
-
执行权竞争结果:
- CAS成功的线程:获得函数执行权,开始执行目标函数。
- CAS失败的线程:状态已被其他线程抢先改为“执行中”,因此进入挂起等待队列。
-
执行结果处理:
- 函数成功执行:执行线程将
once_flag 状态原子性地更新为“已完成”,并唤醒所有等待的线程。
- 函数抛出异常:执行线程将状态回滚为“未执行”,并唤醒等待线程,后续仍会有线程重新竞争执行权。

经典应用:线程安全的单例模式
单例模式是 std::call_once 最典型的应用场景。相较于手写双重检查锁定,使用 call_once 实现的单例模式代码更简洁、逻辑更安全,并且通常具有更好的性能。
代码示例:使用 call_once 实现单例
#include <mutex>
#include <memory>
#include <iostream>
class Singleton {
private:
static std::once_flag init_flag;
static std::unique_ptr<Singleton> instance;
Singleton() {
std::cout << "Singleton 构造" << std::endl;
}
public:
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
static Singleton& getInstance() {
std::call_once(init_flag, []() {
instance.reset(new Singleton());
});
return *instance;
}
void doSomething() {
std::cout << "执行单例方法" << std::endl;
}
};
// 静态成员初始化
std::once_flag Singleton::init_flag;
std::unique_ptr<Singleton> Singleton::instance;
使用示例
int main() {
// 多个线程同时调用 getInstance() 也是安全的
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
// s1 和 s2 是同一个实例
s1.doSomething();
s2.doSomething();
return 0;
}
性能对比优势
为什么 std::call_once 是更好的选择?我们来看一组性能对比(基于典型实测数据):
| 方案 |
首次调用开销 |
后续调用开销 |
线程安全性 |
| std::call_once |
~80 ns |
~1-2 ns |
✔️ 安全,标准库保证 |
| 双重检查锁定 |
~100-120 ns |
~10-15 ns |
⚠️ 复杂,易出错 |
| 简单互斥锁 |
~150-200 ns |
~150-200 ns |
✔️ 安全,但性能差 |

std::call_once 的核心性能优势在于:
- 快速路径无锁:当
once_flag 状态已为“已完成”时,call_once 仅需一次原子读操作即可返回,开销极小。
- 避免忙等待:在等待其他线程执行时,线程会被正确挂起,释放CPU资源,而不是空转循环。
- 内存序保证:标准库实现会自动插入必要的内存屏障,防止因指令重排导致的可见性问题,开发者无需关心底层细节。
进阶用法与技巧
1. 传递参数
call_once 可以传递参数给要执行的函数,参数会按值拷贝(或移动)到内部存储。
void init_resource(int id, const std::string& config) {
std::cout << "初始化资源: " << id << “, 配置: “ << config << std::endl;
}
std::once_flag init_flag;
void safe_init(int id, const std::string& cfg) {
// 参数 id 和 cfg 会被安全地传递
std::call_once(init_flag, init_resource, id, cfg);
}
2. 异常安全处理
如前所述,call_once 具备异常安全特性。如果执行函数抛出异常,状态会重置。
std::once_flag flag;
try {
std::call_once(flag, [] {
// 初始化逻辑,可能抛出异常
if (some_error) {
throw std::runtime_error(“初始化失败”);
}
});
} catch (const std::exception& e) {
// 异常被捕获,flag 保持“未完成”状态
std::cerr << “初始化失败: “ << e.what() << std::endl;
}
// 下次某个线程调用 call_once(flag, ...) 时,将重新尝试执行
3. 与 RAII 思想结合
你可以将 call_once 封装到类中,实现更复杂的资源生命周期管理。
class OnceInitializer {
std::once_flag flag_;
std::function<void()> cleanup_;
public:
template<typename F>
void run_once(F&& f) {
std::call_once(flag_, [this, func = std::forward<F>(f)]() mutable {
func();
// 注册退出时的清理函数
std::atexit([this] { if (cleanup_) cleanup_(); });
});
}
};
总结
std::call_once 和 std::once_flag 是现代 C++ 并发编程工具箱中不可或缺的组件。它们用极其简洁的 API,封装了底层复杂的同步原语和内存序问题,让开发者能专注于业务逻辑本身,而不用陷入手动管理锁、条件变量以及排查竞态条件的泥潭。
在实际开发中,当你需要确保某个操作(如初始化、加载配置、注册回调等)在多线程环境下绝对只执行一次时,应优先考虑 std::call_once。它不仅是替代手写双重检查锁定的更优方案,在代码可读性、维护性和运行效率上也都表现更佳。
记住,优秀的并发代码并非一定要从零构建复杂的同步机制,善于利用标准库提供的、经过充分验证的高级抽象,往往是更可靠、更高效的选择。
如果你对这类并发编程的实战技巧和深度原理感兴趣,欢迎来 云栈社区 与更多开发者一起交流探讨。