1. 为什么需要线程安全的一次性执行机制?
在C++多线程编程中,我们经常会遇到这样的场景:某个初始化操作只需要执行一次,但可能被多个线程同时触发。比如:
- 单例模式的延迟初始化
- 全局配置的加载
- 系统资源的首次分配
- 回调函数的注册
如果简单地用if判断加锁来实现,代码可能会是这样:
cpp复制std::mutex mtx;
bool initialized = false;
void init_once() {
std::lock_guard<std::mutex> lock(mtx);
if (!initialized) {
// 初始化代码
initialized = true;
}
}
这种实现虽然能工作,但每次调用都要获取锁,性能开销很大。更糟的是,如果多个线程同时调用,只有第一个线程能真正执行初始化,其他线程都在无谓地等待锁。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C++标准库的一次性执行方案
C++11引入了std::call_once和std::once_flag来解决这个问题:
cpp复制#include <mutex>
std::once_flag flag;
void init_resource() {
std::call_once(flag, [](){
// 只会执行一次的初始化代码
});
}
2.1 std::once_flag的工作原理
std::once_flag是一个辅助类,它内部维护了一个原子状态,可能的值包括:
- NOT_CALLED:尚未调用
- IN_PROGRESS:正在执行
- COMPLETED:已完成
当线程调用std::call_once时:
- 检查标志状态
- 如果是NOT_CALLED,尝试原子地将其改为IN_PROGRESS
- 如果修改成功,执行回调函数
- 完成后将状态改为COMPLETED
- 其他线程看到状态不是NOT_CALLED,就会直接返回
2.2 性能优化细节
现代实现通常会采用双重检查锁定模式:
- 首先做一次无锁的快速检查(内存顺序为acquire)
- 如果发现已经完成,直接返回
- 否则进入慢路径,获取锁后再次检查
这种优化使得在初始化完成后,后续调用几乎没有任何性能开销。
3. 手动实现call_once的几种方式
虽然标准库提供了现成方案,但理解其实现原理很有必要。以下是几种常见实现方式:
3.1 基于原子变量的实现
cpp复制class OnceFlag {
std::atomic<int> state{0}; // 0:未执行, 1:执行中, 2:已完成
public:
template<typename Callable>
void call_once(Callable&& f) {
int expected = 0;
if (state.compare_exchange_strong(expected, 1,
std::memory_order_acq_rel)) {
try {
f();
state.store(2, std::memory_order_release);
} catch (...) {
state.store(0, std::memory_order_release);
throw;
}
} else {
while (s
