美文网首页
C++完美单例模式

C++完美单例模式

作者: linanwx | 来源:发表于2018-05-07 13:15 被阅读0次

原始的单例模式

单例模式要做如下事情:

  • 不能通过构造函数构造,否则就能够实例化多个。构造函数需要私有声明
  • 保证只能产生一个实例

下面是一个简单的实现:

class Singleton
{
  private:
    static Singleton *local_instance;
    Singleton(){};

  public:
    static Singleton *getInstance()
    {
        if (local_instance == nullptr)
        {
            local_instance = new Singleton();
        }
        return local_instance;
    }
};

Singleton * Singleton::local_instance = nullptr;

int main()
{
    Singleton * s = Singleton::getInstance();
    return 0;
}

使用局部静态对象来解决存在的两个问题

刚刚的代码中有两个问题,一个是多线程的情况下可能会出现new两次的情况。另外一个是程序退出后没有运行析构函数。
下面采用了静态对象来解决。

class Singleton
{
  private:
    static Singleton *local_instance;
    Singleton(){
        cout << "构造" << endl;
    };
    ~Singleton(){
        cout << "析构" << endl;
    }

  public:
    static Singleton *getInstance()
    {
        static Singleton locla_s;
        return &locla_s;
    }
};


int main()
{
    cout << "单例模式访问第一次前" << endl;
    Singleton * s = Singleton::getInstance();
    cout << "单例模式访问第一次后" << endl;
    cout << "单例模式访问第二次前" << endl;
    Singleton * s2 = Singleton::getInstance();
    cout << "单例模式访问第二次后" << endl;
    return 0;
}
输出结果

该代码可能在c++11之前的版本导致多次构造函数的调用,所以只能在较新的编译器上使用。

如果是c++11之前的版本,静态对象线程会不安全

下面这个版本使用了mutex以及静态成员来析构单例。该方案的劣处在于锁导致速度慢,效率低。但是至少是正确的,也能在c++11之前的版本使用,代码的示例如下:

class Singleton
{
  private:
    static Singleton *local_instance;
    static pthread_mutex_t mutex;
    Singleton(){
        cout << "构造" << endl;
    };
    ~Singleton(){
        cout << "析构" << endl;
    }
    class rememberFree{
        public:
        rememberFree(){
            cout << "成员构造" << endl;
        }
        ~rememberFree(){
            if(Singleton::local_instance != nullptr){
                delete Singleton::local_instance;
            }
        }
    };
    static rememberFree remember;

  public:
    static Singleton *getInstance()
    {
        pthread_mutex_lock(&mutex);
        if (local_instance == nullptr)
        {
            local_instance = new Singleton();
        }
        pthread_mutex_unlock(&mutex);
        return local_instance;
    }
};

Singleton * Singleton::local_instance = nullptr;
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;
Singleton::rememberFree Singleton::remember;

使用双锁检查导致未初始化的内存访问

使用如下的代码来实现已经初始化的对象的直接返回。可以使上述代码性能会大大加快。但是相同的代码在Java下面有很明显的问题,由于CPU乱序执行,可能导致访问到未经初始化的对象的引用。
C++是否有同样的问题呢?看下文: http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
结论是一样的,c++也存在相同的问题,可能导致未定义行为导致段错误。双锁检查代码的例子如下:

static Singleton *getInstance()
    {
        if(local_instance == nullptr){
            pthread_mutex_lock(&mutex);
            if (local_instance == nullptr)
            {
                local_instance = new Singleton();
            }
            pthread_mutex_unlock(&mutex);
        }
        return local_instance;
    }

假如线程A进入锁内并分配对象的空间,但是由于指令可能乱序,实际上导致local_instance被先指向一块未被分配的内存,然后再在这块内存上进程初始化。但是在指向后,未初始化前,另一线程B可能通过getInstance获取到这个指针。

尝试使用局部变量并不能保证指令执行顺序

尝试使用临时变量强制指定指令运行顺序时,仍然会被编译器认为是无用的变量,然后被优化掉。下述代码是一个想法很好但是无法实现目的代码:

        if(local_instance == nullptr){
            static mutex mtx;
            lock_guard<mutex> lock(mtx);
            if (local_instance == nullptr)
            {
                auto tmp = new Singleton()
                local_instance = tmp;            }
        }
        return local_instance;

不优雅的使用volatile来解决指令乱序在双检查锁中出现的问题

尝试使用volatile声明内部的指针,代码如下:

class Singleton
{
  private:
    static Singleton * volatile local_instance;
    Singleton(){
        cout << "构造" << endl;
    };
    ~Singleton(){
        cout << "析构" << endl;
    }
    class rememberFree{
        public:
        rememberFree(){
            cout << "成员构造" << endl;
        }
        ~rememberFree(){
            if(Singleton::local_instance != nullptr){
                delete Singleton::local_instance;
            }
        }
    };
    static rememberFree remember;
    

  public:
    static Singleton *getInstance()
    {
        if(local_instance == nullptr){
            static mutex mtx;
            lock_guard<mutex> lock(mtx);
            if (local_instance == nullptr)
            {
                auto tmp = new Singleton();
                local_instance = tmp;
            }
        }
        return local_instance;
    }
};

Singleton * volatile Singleton::local_instance = nullptr;
Singleton::rememberFree Singleton::remember;

int main()
{
    cout << "单例模式访问第一次前" << endl;
    Singleton * s = Singleton::getInstance();
    cout << "单例模式访问第一次后" << endl;
    cout << "单例模式访问第二次前" << endl;
    Singleton * s2 = Singleton::getInstance();
    cout << "单例模式访问第二次后" << endl;
    return 0;
}

在这份代码中,虽然temp是volatile,但是*temp不是,其成员也不是。所以仍然可能被优化。尝试将其*temp也声明为volatile,你会发现的的代码充满了volatile。但是至少是正确的:

class Singleton
{
  private:
    static volatile Singleton * volatile local_instance;
    Singleton(){
        cout << "构造" << endl;
    };
    ~Singleton(){
        cout << "析构" << endl;
    }
    class rememberFree{
        public:
        rememberFree(){
            cout << "成员构造" << endl;
        }
        ~rememberFree(){
            if(Singleton::local_instance != nullptr){
                delete Singleton::local_instance;
            }
        }
    };
    static rememberFree remember;
    

  public:
    static volatile Singleton *getInstance()
    {
        if(local_instance == nullptr){
            static mutex mtx;
            lock_guard<mutex> lock(mtx);
            if (local_instance == nullptr)
            {
                auto tmp = new Singleton();
                local_instance = tmp;
            }
        }
        return local_instance;
    }
};

volatile Singleton * volatile Singleton::local_instance = nullptr;
Singleton::rememberFree Singleton::remember;

int main()
{
    cout << "单例模式访问第一次前" << endl;
    volatile Singleton * s = Singleton::getInstance();
    cout << "单例模式访问第一次后" << endl;
    cout << "单例模式访问第二次前" << endl;
    volatile Singleton * s2 = Singleton::getInstance();
    cout << "单例模式访问第二次后" << endl;
    return 0;
}

大杀器——内存栅栏

在新的标准中,atomic类实现了内存栅栏,使得多个核心访问内存时可控。这利用了c++11的内存访问顺序可控。下面是代码实现:

class Singleton
{
  private:
    // static volatile Singleton * volatile local_instance;
    static atomic<Singleton*> instance;
    Singleton(){
        cout << "构造" << endl;
    };
    ~Singleton(){
        cout << "析构" << endl;
    }
    class rememberFree{
        public:
        rememberFree(){
            cout << "成员构造" << endl;
        }
        ~rememberFree(){
            Singleton* local_instance = instance.load(std::memory_order_relaxed);
            if(local_instance != nullptr){
                delete local_instance;
            }
        }
    };
    static rememberFree remember;
    

  public:
    static Singleton *getInstance()
    {
        Singleton* tmp = instance.load(std::memory_order_relaxed);
        atomic_thread_fence(memory_order_acquire);
        if(tmp == nullptr){
            static mutex mtx;
            lock_guard<mutex> lock(mtx);
            tmp = instance.load(memory_order_relaxed);
            if (tmp == nullptr)
            {
                tmp = new Singleton();
                atomic_thread_fence(memory_order_release);
                instance.store(tmp, memory_order_relaxed);
            }
        }
        return tmp;
    }
};

atomic<Singleton*> Singleton::instance;
Singleton::rememberFree Singleton::remember;

int main()
{
    cout << "单例模式访问第一次前" << endl;
    Singleton * s = Singleton::getInstance();
    cout << "单例模式访问第一次后" << endl;
    cout << "单例模式访问第二次前" << endl;
    Singleton * s2 = Singleton::getInstance();
    cout << "单例模式访问第二次后" << endl;
    return 0;
}

上述代码可能难以阅读,instance的两次加载可以被乱序执行。但是在此期间内的改动被其他CPU核心观察不到。在muduo一书上,内存栅栏也被评价为大杀器。

使用原子操作的内存顺序

这里有六个内存序列选项可应用于对原子类型的操作:memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, 以及memory_order_seq_cst。除非你为特定的操作指定一个序列选项,要不内存序列选项对于所有原子类型默认都是memory_order_seq_cst。虽然有六个选项,但是它们仅代表三种内存模型:排序一致序列(sequentially consistent),获取-释放序列(memory_order_consume, memory_order_acquire, memory_order_release和memory_order_acq_rel),和自由序列(memory_order_relaxed)。

这里可以采用的模型有:默认的memory_order_seq_cst即顺序一致与memory_order_acquire、memory_order_release即获取释放序列。后者性能可能更好。

待完善

使用pthread_once 或者call_once

前者来自pthread库。后者来自std::atomic。

待完善

相关文章

  • 学而时习之单例模式

    本文主要说明单例模式的概念,应用,以及C++实现。 I、上帝视角看单例模式 1.1 单例模式特点 单例模式需要满足...

  • 单例模式

    单例模式及C++实现代码单例模式4种实现详解 c++11改进我们的模式之改进单例模式 单例模式(Singleton...

  • C++完美单例模式

    原始的单例模式 单例模式要做如下事情: 不能通过构造函数构造,否则就能够实例化多个。构造函数需要私有声明 保证只能...

  • java完美单例模式

    完美单例模式 单例模式的各种概念网上有很多,这里直接贴代码:

  • Singleton 单例模式

    搬运自大神博客单例模式(Singleton)及其C++实现 单例模式,在GOF的《设计模式:可复用面向对象软件的基...

  • 你真的会写单例吗?

    你真的会写单例吗? 摘录来源 单例的正确姿势 Java单例模式可能是最简单也是最常用的设计模式,一个完美的单例需要...

  • 【设计模式】单例模式

    单例模式 常用单例模式: 懒汉单例模式: 静态内部类单例模式: Android Application 中使用单例模式:

  • Android设计模式总结

    单例模式:饿汉单例模式://饿汉单例模式 懒汉单例模式: Double CheckLock(DCL)实现单例 Bu...

  • 老司机来教你单例的正确姿势

    老司机来教你单例的正确姿势 Java单例模式可能是最简单也是最常用的设计模式,一个完美的单例需要做到哪些事呢? 单...

  • C++懒汉式单例模式遇到多线程

    C++懒汉式单例模式遇到多线程 单例模式是一个创建型设计模式, 就是保证在整个程序运行中仅存在该类的一个实例, 比...

网友评论

      本文标题:C++完美单例模式

      本文链接:https://www.haomeiwen.com/subject/asflrftx.html