C++中多线程与Singleton的那些事儿
作者:网络转载 发布时间:[ 2015/3/11 15:34:30 ] 推荐标签:C++ 线程 软件开发
DCL
DCL即double-checked locking。在普通加锁的写法中,每次调用getInstance都会进入临界区,这样在heavy contention的情况下该函数会成为系统性能的瓶颈,这个时候有先驱者们想到了DCL写法,也是进行两次check,当第一次check为假时,才加锁进行第二次check:
template<typename T>
class Singleton
{
public:
static T& getInstance()
{
if(!value_)
{
MutexGuard guard(mutex_);
if (!value_)
{
value_ = new T();
}
}
return *value_;
}
private:
Singleton();
~Singleton();
static T* value_;
static Mutex mutex_;
};
template<typename T>
T* Singleton<T>::value_ = NULL;
template<typename T>
Mutex Singleton<T>::mutex_;
是不是觉得这样完美啦?其实在一段时间内,大家都以为这是正确的、有效的做法。实际上却不是这样的。幸运的是,后来有大牛们发现了DCL中的问题,避免了这样错误的写法在更多的程序代码中出现。
那么到底错在哪呢?我们先看看第12行value_ = new T这一句发生了什么:
分配了一个T类型对象所需要的内存。
在分配的内存处构造T类型的对象。
把分配的内存的地址赋给指针value_
主观上,我们会觉得计算机在会按照1、2、3的步骤来执行代码,但是问题出在这。实际上只能确定步骤1先执行,而步骤2、3的执行顺序却是不一定的。假如某一个线程A在调用getInstance的时候第12行的语句按照1、3、2的步骤执行,那么当刚刚执行完步骤3的时候发生线程切换,计算机开始执行另外一个线程B。因为第一次check没有上锁保护,那么在线程B中调用getInstance的时候,不会在第一次check上等待,而是执行这一句,那么此时value_已经被赋值了,会直接返回*value_然后执行后面使用T类型对象的语句,但是在A线程中步骤3还没有执行!也是说在B线程中通过getInstance返回的对象还没有被构造被拿去使用了!这样会发生一些难以debug的灾难问题。
volatile关键字也不会影响执行顺序的不确定性。
在多核心机器的环境下,2个核心同时执行上面的A、B两个线程时,由于第一次check没有锁保护,依然会出现使用实际没有被构造的对象的情况。
关于DCL问题的详细讨论分析,可以参考Scott Meyer的paper:《C++ and the Perils of Double-Checked Locking》
不过在新的C++11中,这个问题得到了解决。因为新的C++11规定了新的内存模型,保证了执行上述3个步骤的时候不会发生线程切换,相当这个初始化过程是“原子性”的的操作,DCL又可以正确使用了,不过在C++11下却有更简洁的多线程Singleton写法了,这个留在后面再介绍。
关于新的C++11的内存模型,可以参考:C++11中文版FAQ:内存模型、C++11FAQ:Memory Model、C++ Data-Dependency Ordering: Atomics and Memory Model
可能有人要问了,那么有什么办法可以在C++11之前的版本下,使得DCL正确工作呢?要使其正确执行的话,得在步骤2、3直接加上一道memory barrier。强迫CPU执行的时候按照1、2、3的步骤来运行。(经网友@shines77提醒,因没有锁的缘故这里需要用RCU技法,即read-copy-update)
static T& getInstance()
{
if(!value_)
{
MutexGuard guard(mutex_);
if (!value_)
{
T* p = static_cast<T*>(operator new(sizeof(T)));
new (p) T();
// insert some memory barier
value_ = p; // RCU method
}
}
return *value_;
}
也许有人会说,你这已经把先前的value_ = new T()这一句拆成了下面这样的两条语句, 为什么还要在后面插入some memory barrier?
T* p = static_cast<T*>(operator new(sizeof(T)));
new (p) T();
原因是现代处理器都是以Out-of-order execution(乱序执行)的方式来执行指令的。现代CPU基本都是多核心的,一个核包含多个执行单元。例如,一个现代的Intel CPU 包含6个执行单元,可以做一组数学,条件逻辑和内存操作的组合。每个执行单元可以做这些任务的组合。这些执行单元并行地操作,允许指令并行地执行。如果从其它 CPU 来观察,这引入了程序顺序的另一层不确定性。
如果站在单个CPU核心的角度上讲,它(一个CPU核心)看到的程序代码都是单线程的,所以它在内部以自己的“优化方式”乱序、并行的执行代码,然后保证终的结果和按代码逻辑顺序执行的结果一致。但是如果我们编写的代码是多线程的,当不同线程访问、操作共享内存区域的时候,会出现CPU实际执行的结果和代码逻辑所期望的结果不一致的情况。这是因为以单个CPU核心的视角来看代码是“单线程”的。
所以为了解决这个问题,需要memory barrier了,利用它来强迫CPU按代码的逻辑顺序执行。例如上面改动版本的getInstance代码中,因为第10行有memory barrier,所以CPU执行第9、10、11按“顺序”执行的。即使在CPU核心内是并行执行指令(比如一个单元执行第9行、一个单元执行第11行)的,但是他们在退役单元(retirement unit)更新执行结果到通用寄存器或者内存中时也是按照9、10、11顺序更新的。例如一个单元A先执行完了第11行,CPU让单元A等待直到执行第9行的单元B执行完成并在退役单元更新完结果以后再在退役单元更新A的结果。
memory barreir是一种特殊的处理器指令,他指挥处理器做下面三件事:(参考文章Mutex And Memory Visibility)
刷新store buffer。
等待直到memory barreir之前的操作已经完成。
不将memory barreir之后的操作移到memory barreir之前执行。
通过使用memory barreir,可以确保之前的乱序执行已经全部完成,并且未完成的写操作已全部刷新到主存。因此,数据一致性又重新回到其他线程的身边,从而保证正确内存的可见性。实际上,原子操作以及通过原子操作实现的模型(例如一些锁之类的),都是通过在底层加入memory barrier来实现的。
至于如何加入memory barrier,在unix上可以通过内核提供的barrier()宏来实现。或者直接嵌入ASM汇编指令mfence也可以,barrier宏也是通过该指令实现的。
关于memory barreir可以参考文章Memory Barriers/Fences。
Meyers Singleton
Scott Meyer在《Effective C++》中提出了一种简洁的singleton写法
template<typename T>
class Singleton
{
public:
static T& getInstance()
{
static T value;
return value;
}
private:
Singleton();
~Singleton();
};
先说结论:
单线程下,正确。
C++11及以后的版本(如C++14)的多线程下,正确。
C++11之前的多线程下,不一定正确。
原因在于在C++11之前的标准中并没有规定local static变量的内存模型,所以很多编译器在实现local static变量的时候仅仅是进行了一次check(参考《深入探索C++对象模型》),于是getInstance函数被编译器改写成这样了:
bool initialized = false;
char value[sizeof(T)];
T& getInstance()
{
if (!initialized)
{
initialized = true;
new (value) T();
}
return *(reinterpret_cast<T*>(value));
}
于是乎它是不是线程安全的了。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系SPASVO小编(021-61079698-8054),我们将立即处理,马上删除。
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11热门文章
常见的移动App Bug??崩溃的测试用例设计如何用Jmeter做压力测试QC使用说明APP压力测试入门教程移动app测试中的主要问题jenkins+testng+ant+webdriver持续集成测试使用JMeter进行HTTP负载测试Selenium 2.0 WebDriver 使用指南