C++Singleton的DCLP(双重锁)实现以及性能测评

 

本文系原创,转载请注明:http://www.cnblogs.com/inevermore/p/4014577.html

 

根据维基百科,对单例模式的描述是:

确保一个类只有一个实例,并提供对该实例的全局访问。

从这段话,我们可以得出单例模式的最重要特点:

一个类最多只有一个对象

 

单线程环境

 

对于一个普通的类,我们可以任意的生成对象,所以我们为了避免生成太多的类,需要将类的构造函数设置为私有。

所以我们写出第一步:

class Singleton

{

public:

    

private:

    Singleton() { }

};

此时在main中就无法直接生成对象:

Singleton s; //ERROR

那么我们想要获取实例,只能借助于类内部的函数,于是我们添加一个内部的函数,而且必须是static函数(思考为什么):

class Singleton

{

public:

    static Singleton *getInstance()

    {

        return new Singleton;

    }

private:

    Singleton() { }

};
OK,我们可以用这个函数生成对象了,但是每次都去new,无法保证唯一性,于是我们将对象保存在一个static指针内,然后每次获取对象时,先检查该指针是否为空:
class Singleton

{

public:

    static Singleton *getInstance()

    {

        if(pInstance_ == NULL) //线程的切换

        {

            ::sleep(1);

            pInstance_ = new Singleton;

        }

            

        return pInstance_;

    }

private:

    Singleton() { }



    static Singleton *pInstance_;

};



Singleton *Singleton::pInstance_ = NULL;

我们在main中测试:

cout << Singleton::getInstance() << endl;

cout << Singleton::getInstance() << endl;

可以看到生成了相同的对象,单例模式编写初步成功。

 

多线程环境下的考虑

 

但是目前的代码就真的没问题了吗?

我写出了以下的测试:

class TestThread : public Thread

{

public:

    void run()

    {

        cout << Singleton::getInstance() << endl;

        cout << Singleton::getInstance() << endl;

    }

};



int main(int argc, char const *argv[])

{

    //测试证明了多线程下本代码存在竞争问题



    TestThread threads[12];

    for(int ix = 0; ix != 12; ++ix)

    {

        threads[ix].start();

    }



    for(int ix = 0; ix != 12; ++ix)

    {

        threads[ix].join();

    }

    return 0;

}

 

这里注意,为了达到效果,我特意做了如下改动:

if(pInstance_ == NULL) //线程的切换

{

     ::sleep(1);

     pInstance_ = new Singleton;

}

这样故意造成线程的切换

打印结果如下:

0xb1300468

0xb1300498

0x9f88728

0xb1300498

0xb1300478

0xb1300498

0xb1100488

0xb1300498

0xb1300488

0xb1300498

0xb1300498

0xb1300498

0x9f88738

0xb1300498

0x9f88748

0xb1300498

0xb1100478

0xb1300498

0xb1100498

0xb1300498

0xb1100468

0xb1300498

0xb11004a8

0xb11004a8

 

很显然,我们的代码在多线程下经不起推敲。

怎么办?加锁! 于是我们再度改进:

class Singleton

{

public:

    static Singleton *getInstance()

    {

        mutex_.lock();

        if(pInstance_ == NULL) //线程的切换

            pInstance_ = new Singleton;

        mutex_.unlock();

        return pInstance_;

    }

private:

    Singleton() { }



    static Singleton *pInstance_;

    static MutexLock mutex_;

};



Singleton *Singleton::pInstance_ = NULL;

MutexLock Singleton::mutex_;

此时测试,无问题。

但是,互斥锁会极大的降低系统的并发能力,因为每次调用都要加锁,等于一群人过独木桥

我写了一份测试如下:

class TestThread : public Thread

{

public:

    void run()

    {

        const int kCount = 1000 * 1000;

        for(int ix = 0; ix != kCount; ++ix)

        {

            Singleton::getInstance();

        }

    }

};



int main(int argc, char const *argv[])

{

    //Singleton s; ERROR



    int64_t startTime = getUTime();



    const int KSize = 100;

    TestThread threads[KSize];

    for(int ix = 0; ix != KSize; ++ix)

    {

        threads[ix].start();

    }



    for(int ix = 0; ix != KSize; ++ix)

    {

        threads[ix].join();

    }



    int64_t endTime = getUTime();



    int64_t diffTime = endTime - startTime;

    cout << "cost : " << diffTime / 1000 << " ms" << endl;



    return 0;

}

开了100个线程,每个调用1M次getInstance,其中getUtime的定义如下:

int64_t getUTime()

{

    struct timeval tv;

    ::memset(&tv, 0, sizeof tv);

    if(gettimeofday(&tv, NULL) == -1)

    {

        perror("gettimeofday");

        exit(EXIT_FAILURE);

    }

    int64_t current = tv.tv_usec;

    current += tv.tv_sec * 1000 * 1000;

    return current;

}

运行结果为:

cost : 6914 ms

 

 

采用双重锁模式

 

上面的测试,我们还无法看出性能问题,我再次改进代码:

class Singleton

{

public:

    static Singleton *getInstance()

    {

        if(pInstance_ == NULL)

        {

            mutex_.lock();

            if(pInstance_ == NULL) //线程的切换

                pInstance_ = new Singleton;

            mutex_.unlock();

        }

        

        return pInstance_;

    }

private:

    Singleton() { }



    static Singleton *pInstance_;

    static MutexLock mutex_;

};



Singleton *Singleton::pInstance_ = NULL;

MutexLock Singleton::mutex_;

可以看到,我在getInstance中采用了两重检查模式,这段代码的优点体现在哪里?

内部采用互斥锁,代码无论如何是可靠的

new出第一个实例后,后面每个线程访问到最外面的if判断就直接返回了,没有加锁的开销

我再次运行测试,(测试代码不变),结果如下:

cost : 438 ms

啊哈,十几倍的性能差距,可见我们的改进是有效的,仅仅三行代码,却带来了十几倍的效率提升!

 

尾声

 

上面这种编写方式成为DCLP(Double-Check-Locking-Pattern)模式,这种方式一度被认为是绝对正确的,但是后来有人指出这种方式在某些情况下也会乱序执行,可以参考Scott的C++ and the Perils of Double-Checked Locking - Scott Meyer

你可能感兴趣的:(Singleton)