使用C++11实现线程安全的单例模式

相信很多小伙伴,对单例模式很熟悉,但是对于选择哪一种单例模式方案,可能不是特别清楚。

对网上五花百门的实现方式,是不是觉得很头大,到底这些方案都有些啥缺点,啥优点,哪种最完美,可以作为自己的常用代码库。

如果有耐心,请仔细阅读下文,带你回顾一下程序员对于单例模式实现方案的辛酸历程。

没有耐心的话o(*^@^*)o,请直接跳到《C++11实现线程安全单例》章节,你将得到一份完美的多线程安全单例模式代码。

后面我会把完整代码贴出来,供下载。

1、C++11前,程序员们是怎么实现单例模式?

(1)懒汉模式与饿汉模式

首先来了解一下懒汉模式与饿汉模式。

懒汉模式,顾名思义,就是比较懒惰,不是今天必须干的事,坚决放到明天来完成,带有延迟加载的意思。

饿汉模式,意思就是饿了的流浪汉,这种流浪汉什么事情都可以干的,但凡路边的垃圾,街上的妹纸,他都可以吃得下去,饥不择食;

软件一起来,就尽早吃内存,带有提前加载的意思(哪怕暂时用不到)。

我们举一个栗子,比如键盘,一个系统正常输入,我只需要一个键盘,就可以了,所以键盘设计为一个单例,有个打字方法writeWords()。

饿汉模式代码:

class Keyboard
{
public:
    Keyboard() {}
    ~Keyboard() {}
    static Keyboard* instance()
    {
        return _pInstance;
    }

    void writeWords() { }

private:
    static Keyboard* _pInstance;
};

Keyboard* Keyboard::_pInstance = new Keyboard(); 

懒汉模式代码:

class Keyboard
{
public:
    Keyboard() {}
    ~Keyboard() {}
    static Keyboard* instance()
    {
        if (!_pInstance)
        {
            _pInstance = new Keyboard();
        }
        return _pInstance;
    }

    void writeWords() { }

private:
    static Keyboard* _pInstance;
};

Keyboard* Keyboard::_pInstance = NULL; 

(2)禁止构造函数、拷贝构造与赋值函数

既然是单例,肯定不允许外面调用构造函数实例化新对象;也不允许拷贝间接实例化新对象;也不允许对象赋值。

我们改造下上面的懒汉与饿汉。

饿汉模式代码:

class Keyboard
{
private:
    Keyboard() = default;
    ~Keyboard() = default;
    Keyboard(const Keyboard&)=delete;
    Keyboard& operator=(const Keyboard&)=delete;

public:
    static Keyboard* instance()
    {
        return _pInstance;
    }

    void writeWords() { }

private:
    static Keyboard* _pInstance;
};

Keyboard* Keyboard::_pInstance = new Keyboard(); 

懒汉模式代码:

class Keyboard
{
private:
    Keyboard() = default;
    ~Keyboard() = default;
    Keyboard(const Keyboard&)=delete;
    Keyboard& operator=(const Keyboard&)=delete;

public:
    static Keyboard* instance()
    {
        if (!_pInstance)
        {
            _pInstance = new Keyboard();
        }
        return _pInstance;
    }

    void writeWords() { }

private:
    static Keyboard* _pInstance;
};

Keyboard* Keyboard::_pInstance = NULL; 

(3)单例的模板化

现在我们再来举个栗子,现在已有一个键盘单例了,像这样的单例,我们还需要很多个,比如鼠标、显示器、耳机。。。(请忽略合理性)。

假设我们选择懒汉模式,那么我们还需要分别添加Mouse、Displayer、Headset三个类,且其实现单例功能的代码和Keyboard高度类似。

发现了吗,小伙伴,我们在做重复性工作了,so,我们需要将单例类模板化,这样每个不同的类需要实现单例,可以套用一个模板。

饿汉模式代码:

template 
class Singleton
{
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;

public:
    static T* instance() { return _instance; }

private:
    static T* _instance;
};

template 
T* Singleton::_instance = new T(); 

懒汉模式代码:

template 
class Singleton
{
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;

public:
    static T* instance()
    {
        if (!_instance)
        {
            _instance = new T();
        }
        return _instance;
    }

private:
    static T* _instance;
};

template 
T* Singleton::_instance = NULL; 

到此,饿汉模式就是最终版本了,后续不会对它进行改造了。

饿汉模式的缺点:也是比较明显,不使用却占用资源,不支持向单例构造函数传参;

优点:就是节省了运行时间(资源提前加载),另外天生自带线程安全属性(在多线程环境下肯定是线程安全的,因为不存在多线程实例化的问题)。

(4)懒汉模式之线程安全性探索

a.懒汉模式下,在定义_instance 变量时先等于NULL,在调用instance()方法时,再判断是否要赋值。这种模式,并非是线程安全的,因为多个线程同时调用instance()方法,就可能导致有产生多个实例。比如A线程执行到第13行之后,第15行之前,当前_instance==NULL,此时由于线程调度,切到B线程,B线程发现_instance==NULL,则进入new T()进行实例化,实例化完成,返回对象指针,然后某一刻发生线程调度,切回到A线程,A线程从以前被打断的地方继续执行,发现_instance==NULL,则进入new T()进行实例化,这样就出现了2个单例对象。显然这是线程非安全的。

那么,要实现线程安全,就必须加锁,以保证对象实例化的原子性。

改造后的代码:

template 
class Singleton
{
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;

public:
    static T* instance()
    {
        std::lock_guard lock_(m_cs);
        if (!_instance)
        {
            _instance = new T();
        }
        return _instance;
    }

private:
    static T* _instance;
    static std::mutex m_cs;
};

template 
T* Singleton::_instance = NULL; 

template 
std::mutex Singleton::m_cs;

似乎解决了多线程实例化安全性问题,完美?

但是似乎引出了其他的问题,我们在每次调用instance()时,都会调用进一次加/解锁,但是实际上这个锁只在我们第一次创建对象时,用来防止多线程竞争起到作用。对象创建起来后,多线程都是读取操作,没有写入操作,所以就不会有安全性问题,此后的调用,我们无疑浪费了很多资源。

b.此时我们需要引出一个高大上的名称:DCLP(Double-Checked Locking Pattern),即“双检锁。怎么操作?就是加个if判断。

改造后的代码:

template 
class Singleton
{
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;

public:
    static T* instance()
    {
        if (!_instance)
        {
            std::lock_guard lock_(m_cs);
            if (!_instance)
            {
                _instance = new T();
            }
        }
        return _instance;
    }

private:
    static T* _instance;
    static std::mutex m_cs;
};

template 
T* Singleton::_instance = NULL; 

template 
std::mutex Singleton::m_cs;

这样就安全了吗。细想下其实还是不安全的。

注意到_instance = new T(),是一个写操作,前面有一个无锁的读操作。当真正的写操作进行时,前面的读操作存在脏读情况。

另外其他原因:https://blog.csdn.net/flyingleo1981/article/details/45485293?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1

这对于码农来说,经过一番折腾,然而却没有得到一个完美的解决方案,这是残忍的。。。

那么,有没有线程安全的懒汉模式单例实现方案呢?答案是有,还好有你(C++11)。下一节介绍C++11实现线程安全单例。

2、C++11实现线程安全单例(懒汉)

在C++11中提供一种方法,使得函数可以线程安全的只调用一次。即使用std::call_once和std::once_flag。std::call_once是一种lazy load的很简单易用的机制。

懒汉模式改造后的代码:

template 
class Singleton
{
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;

public:
    static T* instance()
    {
        std::call_once(_flag, [&](){
            _instance = new T();
        });
        return _instance;
    }

private:
    static T* _instance;
    static std::once_flag _flag;
};

template 
T* Singleton::_instance = NULL; 

template 
std::once_flag Singleton::_flag;

之前我们的单例,instance()只能创建默认构造函数的对象,但是有时候需要给单例传递参数,那么我们需要对instance()方法进行改造,在c++11中,已经支持了可变参数函数。

然而向单例构造函数中传参,这个需求,饿汉模式就无法实现了。

下面我们继续改造,添加构造函数传参。

懒汉模式改造后的代码:

template 
class Singleton
{
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;

public:
    template 
    static T* instance(Args&&... args)
    {
        std::call_once(_flag, &](){
            _instance = new T(std::forward(args)...);
        });
        return _instance;
    }

private:
    static T* _instance;
    static std::once_flag _flag;
};

template 
T* Singleton::_instance = NULL; 

template 
std::once_flag Singleton::_flag;

一般而言,单例对象无需手动释放,程序结束后,由操作系统自动回收资源。但是为了某些时候特殊处理,我们还是添加上destroy()方法。

改造后的代码:

template 
class Singleton
{
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;

public:
    template 
    static T* instance(Args&&... args)
    {
        std::call_once(_flag, [&](){
            _instance = new T(std::forward(args)...);
        });
        return _instance;
    }

    static void destroy()
    {
        if (_instance)
        {
            delete _instance;
            _instance = NULL;
        }
    }

private:
    static T* _instance;
    static std::once_flag _flag;
};

template 
T* Singleton::_instance = NULL; 

template 
std::once_flag Singleton::_flag;

到此,这里为懒汉模式的最终版,我们姑且称之为懒汉版本1。

在C++11标准中,要求局部静态变量初始化具有线程安全性。

另外还有一个版本的懒汉模式代码,也是支持线程安全(打开编译器C++11支持),大家看看,大概长这样:

class Singleton
{
public:
    static Singleton* instance()
    {
        static Singleton _instance;
        return &s_instance;
    }
private:
    Singleton() {}
};

这里使用了局部静态变量,C++11机制可以保证它的初始化具有原子性,线程安全。

这个对象保存在静态数据区,和全局变量是在一起的,而不是在堆中。

习惯让我感觉对象保存在堆中更好,至于是不是,待解释。

姑且称这个为懒汉版本2。

3、结论(拿干货

经过上面的角逐,现在剩下3位选手:饿汉最终版、懒汉版本1、懒汉版本2。

饿汉与懒汉阵营PK:饿汉资源提前加载,浪费比较严重,尤其有一些功能,用户可以选择性启用的,用户如果不需要,

犯不着一上来就占用额外资源;懒汉不存在资源浪费,且同时具备线程安全。

第一局:懒汉胜利,选择懒汉

个人觉得还是推荐使用懒汉模式,支持线程安全,构造函数传参,手动回收资源。

第二局:懒汉版本1 PK 版本2。

套用上面一句话:习惯让我感觉对象保存在堆中更好,至于是不是,待解释。

所以我们选择懒汉版本1。真是玩笑了,O(∩_∩)O哈哈~

具体用哪个懒汉,看个人喜好吧。

我个人比较倾向于懒汉版本1

下面是测试代码(main.cpp):

#include 
#include 
#include "singleton.h"

class Keyboard
{
public:
    Keyboard(int a = 0, float b = 0.0)
    {
        std::cout << "Keyboard():" << (a+b) << std::endl;
    }

    ~Keyboard()
    {
        std::cout << "~Keyboard()" << std::endl;
    }

    void writeWords()
    {
        std::cout << "I'm writing! addr : " << (int)this << std::endl;
    }
};

int main(int argc, char *argv[])
{
    Keyboard* t1 = Singleton::instance(5, 2.0);
    Keyboard* t2 = Singleton::instance(6, 5.0);
    t1->writeWords();
    t2->writeWords();

    Singleton::destroy();

    QCoreApplication a(argc, argv);

    return a.exec();
}

结果:

使用C++11实现线程安全的单例模式_第1张图片

C++11实现线程安全单例代码和测试代码,下载地址:

https://download.csdn.net/download/u011832525/12306370

 

代码git地址:

https://gitee.com/bailiyang/cdemo/tree/master/DesignPattern/Singleton

 

 

===================================================

===================================================

他来了,他来了,他带着礼物走来了

你可能感兴趣的:(C/C++,c++)