zoukankan      html  css  js  c++  java
  • 再谈单例模式

    1、背景

      最近在看一个大佬写的服务器代码的时候,发现他写的单例模式代码很有趣,开始看的时候没看懂,后面研究了一把,发现这个代码其实就是boost里面的一种单例模式,虽然boost里面有很多单例模式,我们在这里就先研究我看到的这种。

      我们来看最简单的单例模式:

    class QMManager
    {
    public:
        static QMManager &instance()
        {
            static QMManager instance_;
            return instance_;
        }
    }

      这是最简单的版本,在单线程下(或者是C++0X下)是没任何问题的,但在多线程下就不行了,因为static QMManager instance_;这句话不是线程安全的。

      在局部作用域下的静态变量在编译时,编译器会创建一个附加变量标识静态变量是否被初始化,会被编译器变成像下面这样(伪代码):

    static QMManager &instance()
    {
        static bool constructed = false;
        static uninitialized QMManager instance_;
        if (!constructed) {
            constructed = true;
            new(&s) QMManager; //construct it
        }
        return instance_;
    }

       这里有竞争条件,两个线程同时调用instance()时,一个线程运行到if语句进入后还没设constructed值,此时切换到另一线程,constructed值还是false,同样进入到if语句里初始化变量,两个线程都执行了这个单例类的初始化,就不再是单例了。

      一个解决方法是加锁,但这样每次调用instance()都要加锁解锁,代价略大。

    static QMManager &instance()
    {
        Lock(); //锁自己实现
        static QMManager instance_;
        UnLock();
        return instance_;
    }

      那再改变一下,把内部静态实例变成类的静态成员,在外部初始化,也就是在include了文件,main函数执行前就初始化这个实例,就不会有线程重入问题了。

    class QMManager
    {
    protected:
        static QMManager instance_;
        QMManager();
        ~QMManager(){};
    public:
        static QMManager *instance()
        {
            return &instance_;
        }
        void do_something();
    };
    QMManager QMManager::instance_; //外部初始化

      这被称为饿汉模式,程序一加载就初始化,不管有没有调用到。

      看似没问题,但还是有坑,在一个情况下会有问题:在这个单例类的构造函数里调用另一个单例类的方法可能会有问题。

    //Singleton.h
    #include <iostream>
    
    using namespace std;
    
    class QMManager
    {
    protected:
        static QMManager instance_;
        QMManager();
        ~QMManager(){};
    public:
        static QMManager *instance()
        {
            return &instance_;
        }
    };
     
    class QMSqlite
    {
    protected:
        static QMSqlite instance_;
        QMSqlite();
        ~QMSqlite(){};
    public:
        static QMSqlite *instance()
        {
            return &instance_;
        }
        void do_something();
    };
     
    QMManager QMManager::instance_;
    QMSqlite QMSqlite::instance_;
    //Singleton.cpp
    #include "Singleton.h"
    
    QMManager::QMManager()
    {
        printf("QMManager constructor
    ");
        QMSqlite::instance()->do_something();
    }
     
    QMSqlite::QMSqlite()
    {
        printf("QMSqlite constructor
    ");
    }
    void QMSqlite::do_something()
    {
        printf("QMSqlite do_something
    ");
    }
    
    int main()
    {
        return 0;
    }

      这里QMManager的构造函数调用了QMSqlite的instance函数,但此时QMSqlite::instance_可能还没有初始化。

      这里的执行流程:程序开始后,在执行main前,执行到QMManager QMManager::instance_;这句代码,初始化QMManager里的instance_静态变量,调用到QMManager的构造函数,在构造函数里调用QMSqlite::instance(),取QMSqlite里的instance_静态变量,但此时QMSqlite::instance_还没初始化,问题就出现了。

      那这里会crash吗,测试结果是不会,这应该跟编译器有关,静态数据区空间应该是先被分配了,在调用QMManager构造函数前,QMSqlite成员函数在内存里已经存在了,只是还未调到它的构造函数,所以输出是这样:

      QMManager constructor
      QMSqlite do_something
      QMSqlite constructor

    2、boost的单例

    2.1 初级boost原理

      那上面问题怎么解决呢,单例对象作为静态局部变量有线程安全问题,作为类静态全局变量在一开始初始化,有以上问题,那结合下上述两种方式,可以解决这两个问题。boost的实现方式是:单例对象作为静态局部变量,但增加一个辅助类让单例对象可以在一开始就初始化。

    //Singleton.h
    class QMManager
    {
    protected:
        struct object_creator
        {
            object_creator()
            {
                QMManager::instance();
            }
            inline void do_nothing() const {}
        };
        static object_creator create_object_;
     
        QMManager();
        ~QMManager(){};
    public:
        static QMManager *instance()
        {
            static QMManager instance;
            return &instance;
        }
    };
    
    class QMSqlite
    {
    protected:
        QMSqlite();
        ~QMSqlite(){};
        struct object_creator
        {
            object_creator()
            {
                QMSqlite::instance();
            }
            inline void do_nothing() const {}
        };
        static object_creator create_object_;
    public:
        static QMSqlite *instance()
        {
            static QMSqlite instance;
            return &instance;
        }
        void do_something();
    };
     
    QMManager::object_creator QMManager::create_object_;
    QMSqlite::object_creator QMSqlite::create_object_;

      结合上面的的cpp,这下可以看到正确的输出和调用了:

      QMManager constructor
      QMSqlite constructor
      QMSqlite do_something

      来看看这里的执行流程:

      初始化QMManager类全局静态变量create_object_
      ->调用object_creator的构造函数
      ->调用QMManager::instance()方法初始化单例
      ->执行QMManager的构造函数
      ->调用QMSqlite::instance()
      ->初始化局部静态变量QMSqlite instance
      ->执行QMSqlite的构造函数,然后返回这个单例。

      跟最后一个例子的区别在于QMManager调用QMSqlite单例时,例子是取到全局静态变量,此时这个变量未初始化,而boost的单例是静态局部变量,此时调用会初始化。

      跟最初例子的区别是在main函数前就初始化了单例,不会有线程安全问题。

    2.2、最终boost

      上面为了说明清楚点去除了模版,实际使用是用模版,不用写那么多重复代码,这是boost库的模板实现:

    //Singleton.h
    #include <iostream>
    
    using namespace std;
    
    template <typename T>
    class Singleton
    {
    private:
        struct object_creator
        {
            object_creator() 
            {
                Singleton<T>::instance(); 
            }
            inline void do_nothing() const {}
        };
        //利用类的静态对象object_creator的构造初始化,在进入main之前已经调用了instance
        //从而避免了多次初始化的问题
        static object_creator create_object_;
    public:
        static T *instance()
        {
            static T obj;
            //do_nothing 是必要的,do_nothing的作用有点意思,
            //如果不加create_object_.do_nothing();这句话,在main函数前面
            //create_object_的构造函数都不会被调用,instance当然也不会被调用,
            //这是模板的编译(模版的延迟实现)导致,如果没有这句话,编译器也不会实现
            // Singleton<T>::object_creator,所以就会导致这个问题
            create_object_.do_nothing();
            return &obj;
        }
    };
    //因为create_object_是类的静态变量,必须有一个通用的声明
    template <typename T>  
    typename Singleton<T>::object_creator Singleton<T>::create_object_;
    
    //测试的例子
    class Object_A
    {
        //其实使用友元帮助我们可以让Object_2B的构造函数是protected的,从而真正实现单子的意图
        friend class Singleton<Object_A>;
        //注意下面用protected,大家无法构造实例
    protected:
        Object_A(){ cout << "Object_A construct." << endl; };
        ~Object_A(){};
    public:
        void do_something() { cout << "do something." << endl; };
    protected:
        int data_a_1;
    };
    //Singleton.cpp
    #include "Singleton.h"
    
    int main()
    {
        cout << "enter main." << endl;
        
        Object_A *a = Singleton<Object_A>::instance();
    
        a->do_something();
    }

      首先BOOST的这个实现的Singleton的数据分成两个部分,一个是内部类的object_creator的静态成员creator_object_,一个是instance函数内部的静态变量static T obj;如果外部的有人调用了instance()函数,静态变量obj就会被构造出来,而静态成员creator_object_会在main函数前面构造,进入main之前,唯一的主线程开始构造Singleton<T>::create_object,在其构造函数之内调用 Singleton的instance函数,并在该函数内生成Singleton对象,所以肯定是线程安全的。他的构造函数内部也调用instance(),这样就会保证静态变量一定会在main函数前面初始化出来。

      到此为止,这部分还都能正常理解,但instance()函数中的这句就是有点诡异技巧的了。

    create_object_.do_nothing();

      其实这句话如果单独分析,并没有明确的作用,因为如果类的静态成员creator_object_的构造就应该让单子对象被初始化。但一旦你注释掉这句话,你会发现create_object_的构造函数都不会被调用。在main函数之前,什么事情都没有发生(VC++2013和GCC都一样),BOOST的代码注释只说是确保create_object_的构造被调用,但也没有明确原因。

    //有do_nothing的执行结果
    Object_A construct.
    enter main.
    do something.
    
    //注释do_nothing的执行结果
    enter main.
    Object_A construct.
    do something.

      我估计这还是和模版的编译有潜在的关系,模版都是Lazy Evaluation。所以如果编译器没有编译过create_object_.do_nothing();编译器就会漏掉create_object_的对象一切实现,也就完全不会编译Singleton<T>::object_creator和Singleton<T>:: create_object_代码,所以就会导致这个问题。使用dumpbin 分析去掉前后的obj文件,大约可以证明这点。所以create_object_.do_nothing();这行代码必须要有。

  • 相关阅读:
    6.让代码更具可读性
    5构造函数和析构函数
    4面向对象之类的继承
    3隐形的指针
    2面向对象之类的封装
    od快捷键
    1.纠结的c++
    101宏定义的其他用法
    100解剖宏定义函数
    99,printf scanf手动功能实现
  • 原文地址:https://www.cnblogs.com/ChinaHook/p/8044867.html
Copyright © 2011-2022 走看看