单例模式定义

保证一个类仅有一个实例,并提供一个该实例的全局访问点。
——《设计模式》

代码一

#include <stdlib.h>
using namespace std;

class Singleton {
public:
    static Singleton* GetInstance(){
        if(_instance == nullptr){
            _instance = new Singleton();
        }
        return _instance;
    }

private:
    Singleton(){};  //普通构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷贝构造
    Singleton& operator=(const Singleton &) = delete; //拷贝赋值构造
    Singleton(Singleton &&) = delete;  //移动构造
    Singleton& operator=(Singleton &&) = delete; //移动拷贝构造

    static Singleton *_instance;
}
Singleton* Singleton::_instance = nullptr;

代码一说明

在单例模式中,为了确保一个类对应一个实例,不仅要考虑只能创建一个实例,而且要考虑创建一个实例后,这个实例又被调用去创建另一个实例。所以,要将普通构造和析构函数设置为私有,然后禁用四类特殊构造函数。在这里就使用了c++11的新特性:Singleton(const Singleton &) = delete,可以实现将某个构造函数禁用的功能。

代码一的问题

没有安全地释放内存,而且私有的析构函数不能在外部调用,无法灵活地释放动态内存,容易造成内存泄漏,所以需要加一个静态的释放内存的接口。

代码二

#include <stdlib.h>
using namespace std;

class Singleton {
public:
    static Singleton* GetInstance(){
        if(_instance == nullptr){
            _instance = new Singleton();
            atexit(Destructor);
        }
        return _instance;
    }

private:
    static void Destructor(){
        if(nullptr != _instance){
            delete _instance;
            _instance = nullptr;
        }
    }
    Singleton(){};  //普通构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete; //拷贝构造
    Singleton& operator=(const Singleton &) = delete; //拷贝赋值构造
    Singleton(Singleton &&) = delete;  //移动构造
    Singleton& operator=(Singleton &&) = delete; //移动拷贝构造

    static Singleton *_instance;
}
Singleton* Singleton::_instance = nullptr;

代码二的说明

代码二增加了一个Destructor接口,然后使用atexit确保在程序结束时进行适当的清理工作。

代码二的问题

代码二只是atexit函数本身是线程安全的,但是GetInstance函数并不是线程安全的。例如,当多个线程刚好同时调用GetInstance函数,那么导致的结果是拥有多个全局实例,违背了单例模式的定义。这时候我们自然想到的是加锁。

代码三

#include <stdlib.h>
#include <mutex>
#include <iostream>
using namespace std;
class Singleton{
public:
    static Singleton *GetInstance(){
        if(_instance == nullptr){
        //RAII 利用类的生命周期进行资源管理
            lock_guard<mutex> lock(_mutex);
            if(_instance == nullptr){
                _instance = new Singleton();
                    // operator new
				    // 1. 分配内存
                    // 2. 调用构造函数
                    // 3.返回指针
                    // 多线程环境下 cpu reorder操作
                atexit(Destructor);
            }
        }
        return _instance;
    }

private:
    static void Destructor(){
        if(nullptr != _instance){
            delete _instance;
            _instance = nullptr;
        }
    }
    Singleton(){};  //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete;
    Singleton& operator=(const Singleton &) = delete;
    Singleton(Singleton &&) = delete;
    Singleton& operator=(Singleton &&) = delete;
    static Singleton *_instance;
    static mutex _mutex;
};

Singleton *Singleton::_instance = nullptr;
mutex Singleton::_mutex;

代码三的说明

代码三中使用了线程锁lock_guard lock(_mutex),并且使用了双重检测技术(double check),来优化锁的调用。因为锁的调用,只在刚开始创建实例的时候使用一次,在使用锁之前再加一次判断,可在后来再次调用GetInstance时,直接判断,然后放回,而无需再加锁了。
注意的是,线程锁就是c++11的又一新特性,在本例中lock_guard lock(_mutex)可以在调用的声明周期结束的时候,自动解锁。

代码三的问题

代码三虽然优化了许多,但是这只适合跑单核的处理器,而现在普遍使用多核的处理器的环境下,系统会对程序进行优化,如:编译器重排、cpu重排,使用单核的思维去编程是会出现一些问题的,如:可见性问题、执行序问题。
例如上面例子中,上面的执行顺序本来是1->2->3的,后来被优化,变成了1->3->2,也就是先返回指针,在调用构造函数,为堆块空间赋初值。这样的顺序会造成不好的结果,因为当线程返回指针后,此时的_instance指针就非空了,然后其它线程就可以直接访问堆块空间。而此时的堆块空间可能还没有被构造,是一块空白区域。

代码四

#include <stdlib.h>
#include <mutex>
#include <atomic>
using namespace std;
class Singleton {
public:
    static Singleton *GetInstance(){
        Singleton* tmp = _instance.load(memory_order_acquire);//获取内存屏障
        if(tmp == nullptr){
            lock_guard<std::mutex> lock(_mutex);
            tmp = _instance.load(memory_order_relaxed);
            if(tmp == nullptr){
                tmp = new Singleton;
                _instance.store(tmp, memory_order_release);//释放内存屏障
                atexit(Destructor);
            }
        }
        return tmp;
    }

private:
    static void Destructor(){
        Singleton* tmp = _instance.load(memory_order_relaxed);
        if(nullptr != tmp){
            delete tmp;
        }
    }
    Singleton(){};  //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete;
    Singleton& operator=(const Singleton &) = delete;
    Singleton(Singleton &&) = delete;
    Singleton& operator=(Singleton &&) = delete;
    static Singleton *_instance;
    static mutex _mutex;
};
std::atomic<Singleton*> Singleton::_instance;
std::mutex Singleton::_mutex;

代码四说明

代码四使用了一些c++11新特性来解决代码三的问题。其中引入了两个关键:原子变量 和 内存栏栅。
什么是原子变量呢
他是c++11多线程中,使用无锁编程的基础。原子变量通过提供原子操作,确保对变量的读写操作是不可分割的,即在任何时刻都不会被中断或出现竞争条件。它在实现锁的功能的同时(即临界资源的读写问题),使多线程并发性能更加高效,代码也更加简洁。
什么是内存栏栅呢?
其也叫内存屏障,c++11中提供了6大内存模型

enum memory_order {   
    memory_order_relaxed,    
    memory_order_consume,   
    memory_order_acquire,   
    memory_order_release,    
    memory_order_acq_rel,    
    memory_order_seq_cst
};

它是为了解决多线程中的执行序问题,说起来会很复杂,如果大家有详细了解的需求,点赞+关注,我会在下一期博客用更加小白的话给大家讲解。

好,回归上面例子。说白了就是在获取内存屏障和释放内存屏障之间的代码,就算被程序优化,也不会对其它线程造成影响,它执行的结果和没有被优化的结果相同。
注意:

        Singleton* tmp = _instance.load(memory_order_acquire);//获取内存屏障
        if(tmp == nullptr){
            lock_guard<std::mutex> lock(_mutex);
            tmp = _instance.load(std::memory_order_relaxed);
            if(tmp == nullptr){
                tmp = new Singleton;
                _instance.store(tmp, memory_order_release);//释放内存屏障

上面代码中把原子变量的读取操作和内存栅栏融合在了一起,其等价于以下代码:

        Singleton* tmp = _instance.load(memory_order_relaxed);
        atomic_thread_fence(memory_order_acquire);//获取内存屏障
        if(tmp == nullptr){
            lock_guard<std::mutex> lock(_mutex);
            tmp = _instance.load(memory_order_relaxed);
            if(tmp == nullptr){
                tmp = new Singleton;
                atomic_thread_fence(memory_order_release);//释放内存屏障
                _instance.store(tmp, memory_order_relaxed);

代码四的问题

为了写一个线程安全的程序,代码变得太长太复杂了,下面是一个大神提出的优化思路

代码五

// c++11 magic static 特性:如果变量在初始化的时候,并
//发同时进入声明语句,并发线程将会阻塞等待初始化结束
class Singleton{
public:
    static Singleton& GetInstance(){
        static Singleton instance;
        return instance;
    }
private:
    Singleton(){};  //构造
    ~Singleton(){};
    Singleton(const Singleton &) = delete;
    Singleton& operator=(const Singleton &) = delete;
    Singleton(Singleton &&) = delete;
    Singleton& operator=(Singleton &&) = delete;
}

/*
版本五具备所有优点:
1.利用静态局部变量特性,延迟加载;
2.利用静态局部变量特性,系统自动回收内存,自动调用析构函数
3.静态局部变量初始化时,没有new操作带来的cpu指令reorder操作
4.c++11 静态局部变量初始化时,具备线程安全
*/

代码五的说明

代码五利用了static的新特性,确保了多线程中,只能有一个线程可以初始化_instance,其它阻塞等待,然后返回值是_instance的引用,所以保持了唯一的实例。(总的来说,用得好不如用得巧)

代码五的问题

代码五的另一问题是代码的复用性差,要进一步改动

代码六

template<typename T>
class Singleton{
public:
    static T& GetInstance(){
        static T instance;
        return T instance;
    }
protected:
    virtual ~Singleton(){}
    Singleton(){} //protected修饰构造函数,才能让子类继承
private:
    Singleton(const Singleton &) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton &&) = delete;
    Singleton& operator=(Singleton &&) = delete;
};

class DesignPattern : public 
Singleton<DesignPattern>{
    friend class Singleton<DesignPattern>;
private:
    DesignPattern(){}
    ~DesignPattern(){}
};

代码六说明

代码六就是引入了模板功能,这个就非常考验大家的c++的功底了。值得说明的是,在继承了Singleton后,还使用friend友元的原因是,为了在初始化_instance时,父类可以访问子类中的构造函数,因为子类的构造函数是私有的,这个情况就是“亦父亦友”的情形,比较经典。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐