在多线程编程中,我们经常会遇到需要初始化全局资源或者单例对象的场景,如果多个线程同时检测到资源未初始化并同时执行初始化逻辑,就会出现重复初始化的问题,轻则浪费系统资源,重则导致程序逻辑错误。c++11标准引入的std::call_once和std::once_flag就是专门用来解决这类线程安全初始化问题的工具,它们可以保证指定的初始化操作在多线程环境下只会被执行一次。

std::once_flag和std::call_once的基本作用
std::once_flag是一个辅助类,用来标记某个初始化操作是否已经执行过,它本身不存储任何初始化逻辑,仅作为状态标识使用。而std::call_once是一个函数模板,接收std::once_flag对象和需要执行的初始化可调用对象,保证该可调用对象只会被执行一次。
这两个组件的核心价值在于:不需要开发者手动编写加锁、判断状态、解锁的复杂逻辑,就可以实现线程安全的单次初始化,避免了手动实现时可能出现的死锁、漏判状态等问题。
基本使用方法
使用std::call_once和std::once_flag的流程非常简单,只需要三步:
- 定义一个
std::once_flag类型的全局或静态变量,作为初始化操作的状态标识 - 编写需要执行的初始化逻辑,可以是函数、lambda表达式、函数对象等可调用对象
- 在需要初始化的地方调用
std::call_once,传入std::once_flag对象和初始化可调用对象
基础示例:线程安全的全局变量初始化
下面是一个简单的示例,模拟多个线程同时尝试初始化一个全局配置对象,保证配置只被初始化一次:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex> // 包含std::call_once和std::once_flag的头文件
// 全局配置结构体
struct GlobalConfig {
int max_connection;
std::string log_path;
};
// 全局配置指针,初始为空
GlobalConfig* g_config = nullptr;
// 初始化状态标识
std::once_flag g_config_init_flag;
// 初始化配置的函数
void init_global_config() {
std::cout << "执行全局配置初始化" << std::endl;
g_config = new GlobalConfig();
g_config->max_connection = 100;
g_config->log_path = "/var/log/app.log";
}
// 线程执行的函数,尝试获取配置
void thread_get_config() {
// 调用call_once,保证init_global_config只执行一次
std::call_once(g_config_init_flag, init_global_config);
// 后续使用配置
std::cout << "线程获取配置,最大连接数:" << g_config->max_connection << std::endl;
}
int main() {
// 创建5个线程同时尝试获取配置
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(thread_get_config);
}
// 等待所有线程执行完成
for (auto& t : threads) {
t.join();
}
// 释放配置内存
delete g_config;
return 0;
}
运行上述代码,你会发现"执行全局配置初始化"这句话只会输出一次,即使有5个线程同时调用了std::call_once,初始化逻辑也只会被执行一次,后续线程会直接跳过初始化步骤,使用已经初始化好的配置对象。
使用lambda表达式作为初始化逻辑
如果初始化逻辑比较简单,也可以直接使用lambda表达式作为std::call_once的可调用对象,不需要单独定义初始化函数:
#include <iostream>
#include <thread>
#include <mutex>
class Singleton {
public:
static Singleton* get_instance() {
static std::once_flag init_flag;
std::call_once(init_flag, [&]() {
std::cout << "初始化单例对象" << std::endl;
instance_ = new Singleton();
});
return instance_;
}
private:
Singleton() = default;
static Singleton* instance_;
};
Singleton* Singleton::instance_ = nullptr;
void thread_func() {
Singleton* obj = Singleton::get_instance();
std::cout << "线程获取单例地址:" << obj << std::endl;
}
int main() {
std::thread t1(thread_func);
std::thread t2(thread_func);
std::thread t3(thread_func);
t1.join();
t2.join();
t3.join();
return 0;
}
这个示例实现了一个简单的线程安全单例模式,通过std::call_once保证单例对象的初始化只执行一次,避免了传统双重检查锁实现单例时可能出现的指令重排问题。
使用注意事项
在使用std::call_once和std::once_flag时,需要注意以下几点:
std::once_flag对象不能被拷贝或者移动,也不能被销毁后再次使用,通常定义为全局变量、静态变量或者长期存在的对象成员- 如果初始化可调用对象抛出了异常,那么
std::call_once会认为初始化没有完成,后续其他线程调用时还会再次尝试执行初始化逻辑,直到初始化成功为止 std::call_once的初始化逻辑中如果包含加锁操作,需要注意避免死锁,比如不要在初始化逻辑中等待其他也在调用std::call_once的线程std::once_flag不需要手动重置状态,它的状态一旦标记为已初始化,就会一直保持,无法回到未初始化状态
和其他初始化方式的对比
在c++11之前,开发者通常会用互斥锁加判断的方式实现线程安全初始化,比如下面的传统实现方式:
#include <mutex>
std::mutex g_init_mutex;
bool g_inited = false;
GlobalConfig* g_config = nullptr;
void init_config_old() {
std::lock_guard<std::mutex> lock(g_init_mutex);
if (!g_inited) {
g_config = new GlobalConfig();
g_inited = true;
}
}
这种实现方式存在两个问题:一是每次调用都需要加锁判断,即使初始化已经完成,还是会有锁的开销;二是如果开发者忘记加锁或者判断逻辑写错,就会出现线程安全问题。而std::call_once的实现通常会利用系统底层的单次初始化机制,初始化完成后不会有额外的性能开销,同时也不需要开发者手动处理状态判断,更加安全可靠。
另外c++11之后还支持静态局部变量的线程安全初始化,即静态局部变量的初始化在多线程环境下只会执行一次,不过这种方式只适用于静态局部变量的场景,而std::call_once可以用于任意作用域的初始化逻辑,使用场景更加灵活。
适用场景总结
std::call_once和std::once_flag主要适用于以下场景:
- 全局资源、配置对象的线程安全初始化
- 单例模式的对象初始化
- 只需要执行一次的初始化逻辑,比如日志系统初始化、网络库初始化等
- 需要在多个线程中触发,但只能执行一次的任意可调用逻辑
总的来说,这两个组件是c++多线程编程中非常实用的工具,能够帮助开发者用极低的成本实现线程安全的单次初始化,减少手动编写线程同步逻辑带来的错误风险。
std::call_oncestd::once_flag线程安全初始化C++修改时间:2026-07-06 10:03:37