今天学习了一下c++中的singleton。google了一篇论文C++ and the Perils of Double-Checked Locking。大名鼎鼎的Scott Meyers写的。论文使用c++讲解,看了之后受益匪浅。
巧的是,读完之后刚好看见http://coolshell.cn酷壳站长陈皓大哥的一篇文章http://blog.csdn.net/haoel/article/details/4028232也是讲的这个问题。不同于上面那篇文论,陈皓大哥用的是java讲解。
我想做的是,还原一下那篇论文,算是一个学习总结吧。也是对陈皓大哥那篇文章的一个补充吧。
你去google一下设计模式这四个字,肯定有提到单例(singleton)这个经典设计模式。but but but,,,,,传统的单例模式实现不是线程安全的!!!
为了解决线程安全问题,程序员们做了很多努力,目前最流行的就是double-checked locking pattern(dclp). dclp可为共享资源(比如singleton)添加有效的线程安全。
but but but DCLP也有不足之处,比如不可复用,不能方便地移植,并且在多核处理器系统中也起不到作用。今儿不说这些不足,只说说DCLP怎么解决线程安全问题的。
单例和多线程
传统的单例模式实现:
class Singleton
{
private:
Singleton(){}
public:
static Singleton* instance()
{
if(_instance == 0)
{
_instance = new Singleton();
}
return _instance;
}
private:
static Singleton* _instance;
public:
int atestvalue;
};
Singleton* Singleton::_instance = 0;
上面这种实现在单线程环境下是没有问题的,可是多线程下就有问题了。
稍微分析一下:
1. 例如线程A进入函数instance执行判断语句,这句执行后就挂起了,这时线程A已经认为_instance为NULL,但是线程A还没有创建singleton对象。
2. 又有一个线程B进入函数instance执行判断语句,此时同样认为_instance变量为null,因为A没有创建singleton对象。线程B继续执行,创建了一个singleton对象。
3. 稍后,线程A接着执行,也创建了一个新的singleton对象。
4. fuck!!两个对象!
从上面分析可以看出,需要对_instance变量加上互斥锁:
Singleton* Singleton::instance() {
Lock lock; // acquire lock (params omitted for simplicity)
if (_instance == 0) {
_instance = new Singleton;
}
return _instance;
} // release lock (via Lock destructor)
上锁后是解决了线程安全问题,但是有些资源浪费。稍微分析一下:每次instance函数调用时候都需要请求加锁,其实并不需要,instance函数只需第一次调用的时候上锁就行了。这时可以用DCLP解决。
Double-Checked Locking Pattern
Singleton* Singleton::instance() {
if (_instance == 0) { // 1st test
Lock lock;
if (_instance == 0) { // 2nd test
_instance = new Singleton;
}
}
return _instance;
}
DCLP and Instruction Ordering
我们来仔细打量一下这句代码:
_instance = new singleton()
为了执行这句代码,机器需要做三样事儿:
1.singleton对象分配空间。
2.在分配的空间中构造对象
3.使_instance指向分配的空间
遗憾的是编译器并不是严格按照上面的顺序来执行的。可以交换2和3.
将上面三个步骤标记到代码中就是这样:
Singleton* Singleton::instance() {
if (_instance == 0) {
Lock lock;
if (_instance == 0) {
_instance = // Step 3
operator new(sizeof(Singleton)); // Step 1
new (_instance) Singleton; // Step 2
}
}
return _instance;
}
好了,紧张的时刻到了,如果发生下面两件事:
- 线程A进入了instance函数,并且执行了step1和step3,然后挂起。这时的状态是:_instance不NULL,而_instance指向的内存去没有对象!
- 线程B进入了instance函数,发现_instance不为null,就直接return _instance了。
貌似这时无法解决的问题了,咋办呢。搞嵌入式的程序员可能想到用c++中的volatile关键字。对,就是用volatile,但是用volatile就要一用到底,用了之后就是下面这种丑陋的代码了。
class Singleton {
public:
static volatile Singleton* volatile instance();
...
private:
// one more volatile added
static Singleton* volatile _instance;
};
// from the implementation file
volatile Singleton* volatile Singleton::_instance = 0;
volatile Singleton* volatile Singleton::instance() {
if (_instance == 0) {
Lock lock;
if (_instance == 0) {
// one more volatile added
Singleton* volatile temp = new Singleton;
_instance = temp;
}
}
return _instance;
}
其实上面完全使用volatile关键字的代码也不能保证正常工作在多线程环境中。具体原因分析请参考C++ and the Perils of Double-Checked Locking这篇论文,文章也给出了终极解决方法。