在多线程开发中,很多场景需要保证某个操作只执行一次,比如全局配置初始化、单例对象创建等,C++标准库提供的std::call_once就是专门应对这类需求的工具,它能在多线程竞争的情况下确保目标函数仅被调用一次。

std::call_once基本用法
使用std::call_once需要配合std::once_flag类型的标记,once_flag对象需要在所有线程可见的作用域中定义,通常作为全局变量或者类的静态成员。std::call_once的函数原型如下:
#include <mutex>
#include <thread>
#include <iostream>
// 定义once_flag标记,必须是全局或者静态的,不能被拷贝
std::once_flag init_flag;
// 需要单次执行的函数
void init_config() {
std::cout << "执行配置初始化操作" << std::endl;
}
void worker_thread() {
// 调用call_once,传入标记和要执行的函数
std::call_once(init_flag, init_config);
}
int main() {
// 创建5个线程同时执行worker_thread
std::thread t1(worker_thread);
std::thread t2(worker_thread);
std::thread t3(worker_thread);
std::thread t4(worker_thread);
std::thread t5(worker_thread);
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
return 0;
}
上述代码中,5个线程同时调用worker_thread,但是init_config函数只会执行一次,其他线程调用std::call_once时会直接跳过执行,不会重复触发初始化逻辑。
std::call_once的实战场景:单例模式实现
单例模式是std::call_once最典型的应用场景,传统的单例实现需要考虑多线程下的竞态问题,使用std::call_once可以简化实现逻辑,同时保证线程安全。
#include <mutex>
#include <thread>
#include <iostream>
class Singleton {
private:
Singleton() {
std::cout << "单例对象创建" << std::endl;
}
static std::once_flag init_flag;
static Singleton* instance;
public:
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* get_instance() {
// 保证创建单例的代码只执行一次
std::call_once(init_flag, []() {
instance = new Singleton();
});
return instance;
}
};
// 静态成员初始化
std::once_flag Singleton::init_flag;
Singleton* Singleton::instance = nullptr;
void test_thread() {
Singleton* s = Singleton::get_instance();
std::cout << "线程获取单例地址: " << s << std::endl;
}
int main() {
std::thread t1(test_thread);
std::thread t2(test_thread);
std::thread t3(test_thread);
t1.join();
t2.join();
t3.join();
return 0;
}
运行上述代码可以看到,单例的构造函数只会执行一次,所有线程获取到的单例地址都是同一个,避免了多线程下重复创建单例对象的问题。
使用std::call_once的注意事项
- once_flag对象不能被拷贝或者赋值,否则会导致标记失效,无法保证单次执行。通常once_flag需要定义为全局变量、静态变量或者类静态成员。
- 如果std::call_once调用的函数抛出异常,那么这次调用会被视为未完成,其他线程会再次尝试调用该函数,直到某次调用成功执行。
- std::call_once的第二个参数可以是普通函数、函数对象、lambda表达式,只要可调用即可,支持传入参数,参数需要跟在可调用对象之后传入。
- once_flag的生命周期必须长于所有调用std::call_once的线程,否则会出现未定义行为。
std::call_once和普通加锁的区别
很多开发者可能会想到用互斥锁来实现单次执行,比如先加锁判断标记再执行,但是这种方式需要每次都加锁判断,而std::call_once在第一次执行完成后,后续调用不需要加锁,性能开销更低。另外std::call_once已经处理了所有竞态情况,不需要开发者自己处理判断逻辑,代码更简洁,也不容易出错。
// 普通加锁实现单次执行的示例
#include <mutex>
#include <iostream>
std::mutex mtx;
bool is_inited = false;
void init_with_lock() {
std::lock_guard<std::mutex> lock(mtx);
if (!is_inited) {
std::cout << "执行初始化" << std::endl;
is_inited = true;
}
}
对比可以看出,std::call_once的实现逻辑更简洁,不需要手动维护初始化标记和加锁逻辑,更适合单次执行的场景。
C++std::call_once多线程once_flag修改时间:2026-06-13 23:45:39