引言
在多线程编程中,常常需要确保某些初始化操作只执行一次,例如初始化一个全局资源或单例模式中的实例创建。C++11引入了std::call_once
和std::once_flag
,为这种需求提供了便捷和高效的解决方案。
一、基本概念
1. std::call_once
std::call_once
是一个函数模板,它确保某个函数在多个线程中只被调用一次。其原型如下:
template< class Callable, class... Args > void call_once( std::once_flag& flag, Callable&& func, Args&&... args );
flag
:一个std::once_flag
类型的标志,用来记录是否已经调用过。func
:要执行的函数,可以是函数指针、函数对象或lambda表达式。args
:传递给func
的参数。
2. std::once_flag
std::once_flag
是一个轻量级的同步原语,用于标识某个操作是否已经执行过。默认构造的std::once_flag
处于未调用状态。
二、std::call_once
的使用示例
以下是一个简单示例,演示如何使用std::call_once
和std::once_flag
:
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag;
void initialize() {
std::cout << "Resource initialized." << std::endl;
}
void thread_func() {
std::call_once(flag, initialize);
std::cout << "Thread executed." << 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;
}
在上述代码中,无论多少个线程调用thread_func
,initialize
函数只会执行一次,输出结果如下:
Resource initialized.
Thread executed.
Thread executed.
Thread executed.
//调用其他类的函数
std::call_once(flag,&VideoWidget::threadStart,ui->videoWidget);
三、std::call_once
的实际应用
1. 单例模式
单例模式确保一个类只有一个实例,并提供一个全局访问点。使用std::call_once
可以简化线程安全的单例实现:
#include <iostream>
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, &Singleton::initSingleton);
return *instance;
}
void doSomething() {
std::cout << "Singleton instance doing something." << std::endl;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static void initSingleton() {
instance = new Singleton();
}
static Singleton* instance;
static std::once_flag initFlag;
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;
int main() {
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
s1.doSomething();
s2.doSomething();
return 0;
}
在这个示例中,无论多少个线程调用getInstance
,Singleton
实例只会被创建一次。
2. 全局资源初始化
在多线程程序中,全局资源的初始化需要确保线程安全,例如初始化一个配置文件或数据库连接:
#include <iostream>
#include <thread>
#include <mutex>
#include <fstream>
std::once_flag configFlag;
void loadConfig() {
std::ifstream configFile("config.txt");
if (configFile) {
std::cout << "Configuration loaded." << std::endl;
} else {
std::cout << "Failed to load configuration." << std::endl;
}
}
void thread_func() {
std::call_once(configFlag, loadConfig);
std::cout << "Thread executed." << 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;
}
在上述代码中,loadConfig
函数只会执行一次,从而保证配置文件只被加载一次。
四、std::call_once
的实现原理
std::call_once
通常依赖于底层操作系统提供的同步原语,例如POSIX线程中的pthread_once
。它通过std::once_flag
来记录操作是否已经执行,并使用适当的同步机制(例如互斥锁或原子操作)来保证线程安全。
当多个线程同时调用std::call_once
时,只有一个线程能够成功进入并执行指定的函数,其它线程则会被阻塞,直到函数执行完毕。一旦函数执行完毕,所有等待的线程会继续执行后续代码。
五、注意事项
std::call_once
保证线程安全,但可能会带来一定的性能开销,特别是在多次调用的情况下。std::once_flag
不能被复制或移动,因此必须小心管理其生命周期,确保其在所有线程中可访问。- 被
std::call_once
调用的函数应该是无异常的,因为异常可能会导致std::once_flag
状态的不一致。
六、总结
std::call_once
和std::once_flag
为C++多线程编程提供了简洁而高效的单次调用机制。无论是实现单例模式还是全局资源的初始化,它们都能确保操作的线程安全性。通过合理使用std::call_once
,可以避免复杂的同步代码,使得多线程编程更加可靠和易于维护。