c++中的 单例模式(singleton)和双检测锁(Double-Checked Locking)

今天学习了一下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 (<span style="font-family:CMBX12;">_instance</span> == 0) { // 1st test
Lock lock;
if (<span style="font-family:CMBX12;">_instance</span> == 0) { // 2nd test
<span style="font-family:CMBX12;">_instance</span> = new Singleton;
}
}
return <span style="font-family:CMBX12;">_instance</span>;
}

DCLP and Instruction Ordering

我们来仔细打量一下这句代码:

_instance  = new singleton()
为了执行这句代码,机器需要做三样事儿:

1.singleton对象分配空间。

2.在分配的空间中构造对象

3.使_instance指向分配的空间

遗憾的是编译器并不是严格按照上面的顺序来执行的。可以交换2和3.

将上面三个步骤标记到代码中就是这样:

Singleton* Singleton::instance() {
if (<span style="font-family:CMBX12;">_instance</span> == 0) {
Lock lock;
if (<span style="font-family:CMBX12;">_instance </span>== 0) {
<span style="font-family:CMBX12;">_instance </span>= // Step 3
operator new(sizeof(Singleton)); // Step 1
new (<span style="font-family:CMBX12;">_instance</span>) Singleton; // Step 2
}
}
return <span style="font-family:CMBX12;">_instance</span>;
}

好了,紧张的时刻到了,如果发生下面两件事:

  • 线程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<span style="font-family:CMBX12;"> </span>Singleton* volatile <span style="font-family:CMBX12;">_instance</span>;
};
// from the implementation file
volatile Singleton* volatile Singleton::<span style="font-family:CMBX12;">_instance</span> = 0;
volatile Singleton* volatile Singleton::instance() {
    if (<span style="font-family:CMBX12;">_instance </span>== 0) {
    Lock lock;
    if (<span style="font-family:CMBX12;">_instance </span>== 0) {
// one more volatile added


    Singleton* volatile temp =
      new<span style="font-family:CMBX12;"> </span>Singleton;
    <span style="font-family:CMBX12;">_instance </span>= temp;
}
}
    return <span style="font-family:CMBX12;">_instance</span>;
}

其实上面完全使用volatile关键字的代码也不能保证正常工作在多线程环境中。具体原因分析请参考C++ and the Perils of Double-Checked Locking这篇论文,文章也给出了终极解决方法。


你可能感兴趣的:(c++中的 单例模式(singleton)和双检测锁(Double-Checked Locking))