你好,我是雨乐~
在多线程编程场景中,如果多个线程对同一块数据区域进行读写操作,我们称之为这种存在Data Race
。在这种场景下,第一反应是使用std::mutex
来保证,如果一个变量在初始化之后从未被修改,那么就不需要使用昂贵的锁或者原子操作进行同步。我们只需要确保它在初始化时是线程安全的即可。
今天,聊聊几个常用的线程安全
的初始化方式。
constexpr
常量表达式(constexpr
)是指在编译时就能确定其值的表达式,因此它们天生是线程安全的。另一方面,常量表达式必须在声明时立即初始化,这就进一步保证了其线程安全。
比如像下面这样定义:
constexpr pi = 3.14;
当然了,很多时候,这种元类型不一定能满足我们的需求,比如需要定义成自定义类型或者使用STL的结构。
犹记得首次接触constexpr的时候,将std::map声明为constexpr
,然后报了如下这种编译错误:
error: the type 'const std::map<int, int>' of 'constexpr' variable 'mii' is not literal
187 | constexpr std::map<int, int> mii;
| ^~~
In file included from /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/map:63,
from <source>:7:
/opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/bits/stl_map.h:102:11: note: 'std::map<int, int>' is not literal because:
102 | class map
| ^~~
/opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/bits/stl_map.h:102:11: note: 'std::map<int, int>' does not have 'constexpr' destructor
经过调研后才发现constexpr对其数据类型
有如下限制:
1、不能有虚函数或虚基类
• 如果一个类包含 虚成员函数,或者类的基类是 虚基类,那么该类就不能被用于常量表达式。这是因为虚函数涉及到运行时的动态绑定,而常量表达式的计算必须发生在编译时,无法处理这种运行时的行为
2、构造函数必须是常量表达式
• 该类的 构造函数 必须是常量表达式(即编译时可以求值的函数)。这样,在编译时就可以正确地创建该类的实例,而不依赖运行时的计算
3、每个基类和非静态成员必须被初始化
• 该类的每个 基类 和 非静态成员 都必须在编译时初始化。这意味着,在常量表达式中,所有的成员都必须有明确的初始值,不能存在未初始化的成员
4、成员函数必须是常量表达式
• 类的成员函数,如果要在常量表达式中调用,也必须是常量表达式。也就是说,这些成员函数本身必须是可以在编译时被执行的,不能包含任何需要运行时求值的操作
基于以上要求,一个简单的支持constexpr的类如下:
#include <iostream>
classObj {
public:
constexpr Obj(int32_t num) : num_(num) {}
constexpr int32_t Get() const { return num_; }
private:
int32_t num_;
};
int main() {
constexpr Obj obj{1};
constexprint32_t num = obj.Get();
return0;
}
std::call_once & std::once_flag
在很多情况下,我们不能保证都可以使用constexpr来实现初始化,基于此,C++提供了另外一种方式可以保证在多线程,即结合使用std::call_once & std::once_flag
。
std::call_once 和 std::once_flag 是 C++ 中用于保证某个函数只会执行一次的机制,尤其是在多线程环境下。std::call_once
用于确保一个可调用对象(如函数、lambda 表达式等)只会被执行一次。std::once_flag
是一个标志,用来指示一个操作是否已经执行过。它通常与 std::call_once
一起使用。其主要作用是标记一个操作是否已经成功执行,确保同一个操作(函数)不会在多线程环境中被重复执行。
我们看下如下这个例子:
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag;
void init() {
std::cout << "Initialization function is called!" << std::endl;
}
void thread_function() {
std::call_once(flag, init);
}
int main() {
std::thread t1(thread_function);
std::thread t2(thread_function);
std::thread t3(thread_function);
t1.join();
t2.join();
t3.join();
return0;
}
显然,只会输出一次Initialization function is called!
另一个比较有名的示例就是单例模式,同样,我们可以使用std::call_once & std::once_flag来达到在多线程环境下只会初始化一次。
#include <iostream>
#include <mutex>
#include <thread>
// 单例类
classSingleton {
public:
// 获取单例实例
static Singleton& getInstance() {
std::call_once(flag, []() {
instance.reset(newSingleton()); // 只会被调用一次
});
return *instance;
}
void showMessage() {
std::cout << "Singleton instance is working!" << std::endl;
}
Singleton() { std::cout << "Singleton initialized!" << std::endl; }
~Singleton() { std::cout << "Singleton destroyed!" << std::endl; }
private:
inlinestatic std::unique_ptr<Singleton> instance; // 静态成员,存储单例实例
inlinestatic std::once_flag flag; // 标记单例初始化是否完成
};
void threadFunction() {
Singleton& s = Singleton::getInstance();
s.showMessage();
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
std::thread t3(threadFunction);
t1.join();
t2.join();
t3.join();
return0;
}
static
既然在前面提到了单例模式,那么就不得不提到Meyers Singleton
,由 Scott Meyers 提出的,在其著作《Effective C++》中介绍的单例模式的实现方式,采用了“懒初始化”策略,即在第一次使用单例实例时才创建实例,而不是在程序启动时就创建。
可能你会有所疑问,本文的目的不是线程安全的初始化
么?怎么突然提到了单例模式?
其实,单例模式,在多线程环境下,核心点就要做到线程安全,这就要求在多线程下,只能被实例化一次,而Meyers Singleton
完全满足了该条件,不过Meyers Singleton
的前提是C++11及以上。
#include <iostream>
classSingleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 线程安全的初始化
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
void showMessage() const {
std::cout << "Hello from Singleton!" << std::endl;
}
private:
Singleton() {
std::cout << "Singleton created!" << std::endl;
}
~Singleton() {
std::cout << "Singleton destroyed!" << std::endl;
}
};
int main() {
Singleton& s1 = Singleton::getInstance();
s1.showMessage();
Singleton& s2 = Singleton::getInstance();
s2.showMessage();
return0;
}
下期见~
以上
如果对本文有疑问可以加笔者微信直接交流,笔者也建了C/C++相关的技术群,有兴趣的可以联系笔者加群。