C++单例模式实现的坑 -- 学习一下boost库的单例模式

http://blog.cnbang.net/tech/2229/

单例本来是个很简单的模式,实现上应该也是很简单,但C++单例的简单实现会有一些坑,来看看为了避免这些坑怎样一步步演化到boost库的实现方式。

方案一

1
2
3
4
5
6
7
8
9
class QMManager
{
public :
     static QMManager &instance()
     {
         static QMManager instance_;
         return instance_;
     }
}

这是最简单的版本,在单线程下(或者是C++0X下)是没任何问题的,但在多线程下就不行了,因为static QMManager instance_;这句话不是线程安全的。
在局部作用域下的静态变量在编译时,编译器会创建一个附加变量标识静态变量是否被初始化,会被编译器变成像下面这样(伪代码):

01
02
03
04
05
06
07
08
09
10
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语句里初始化变量,两个线程都执行了这个单例类的初始化,就不再是单例了。

方案二

一个解决方法是加锁:

1
2
3
4
5
6
7
static QMManager &instance()
{
     Lock(); //锁自己实现
     static QMManager instance_;
     UnLock();
     return instance_;
}

但这样每次调用instance()都要加锁解锁,代价略大。

方案三

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class QMManager
{
protected :
     static QMManager instance_;
     QMManager();
     ~QMManager(){};
public :
     static QMManager *instance()
     {
         return &instance_;
     }
     void do_something();
};
QMManager QMManager::instance_; //外部初始化

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

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

看例子:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//.h
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_;

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
//.cpp
QMManager::QMManager()
{
     printf ( "QMManager constructor\n" );
     QMSqlite::instance()->do_something();
}
 
QMSqlite::QMSqlite()
{
     printf ( "QMSqlite constructor\n" );
}
void QMSqlite::do_something()
{
     printf ( "QMSqlite do_something\n" );
}

这里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

方案四

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//.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;
     }
};
QMManager::object_creator QMManager::create_object_;
 
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_;

结合方案3的.cpp,这下可以看到正确的输出和调用了:

QMManager constructor
QMSqlite constructor
QMSqlite do_something

来看看这里的执行流程:

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

跟方案三的区别在于QMManager调用QMSqlite单例时,方案3是取到全局静态变量,此时这个变量未初始化,而方案四的单例是静态局部变量,此时调用会初始化。

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

最终boost

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
template < typename T>
class Singleton
{
     struct object_creator
     {
         object_creator(){ Singleton::instance(); }
         inline void do_nothing() const {}
     };
 
     static object_creator create_object;
 
public :
     typedef T object_type;
     static object_type& instance()
     {
         static object_type obj;
         //据说这个do_nothing是确保create_object构造函数被调用
         //这跟模板的编译有关
         create_object.do_nothing();
         return obj;
     }
 
};
template < typename T> typename Singleton::object_creator Singleton::create_object;
 
class QMManager
{
protected :
     QMManager(){};
     ~QMManager(){};
     friend class Singleton;
public :
     void do_something(){};
};
 
int main()
{
     Singleton::instance().do_something();
     return 0;
}

其实Boost库这样的实现像打了几个补丁,用了一些奇技淫巧,虽然确实绕过了坑实现了需求,但感觉挺不好的。

参考资料:http://blog.csdn.net/fullsail/article/details/8483106


BOOST的Singleton模版详解

首先要说明,这个准确说并不是BOOST的singleton实现,而是BOOST的POOL库的singleton实现。BOOST库中其实有若干个singleton模版,这个只是其中一个。但网上大部分介绍的介绍的BOOST的Singleton实现都是这个,所以大家也就默认了。而且这个的确算是比较特殊和有趣的一个实现。

网上比较有名的文章是这篇《2B程序员,普通程序员和文艺程序员的Singleton实现》 介绍,我虽然对Singleton模版无爱,但自己的项目组中也有人用这个实现,所以还是研究了一下这个实现,特别网上真正解释清楚这个东东的人并不多(包括原文),所以还是研究了一下。

1          为啥2B实现有问题

为了介绍清楚这个实现,我们还要先解释清楚为啥2B实现有问题,首先说明,2B实现和BOOST的实现都可以解决多线程调用Singleton导致多次初始化的问题。


[cpp]  view plain  copy
  1. //H文件  
  2.  template <typename T> class Singleton_2B  
  3.  {  
  4.  protected:  
  5.      typedef  T  object_type;  
  6.      //利用的是类的静态全局变量  
  7.      static T instance_;  
  8.  public:  
  9.      static T* instance()  
  10.      {  
  11.          return &instance_;  
  12.      }  
  13.  };  
  14.    
  15.  //因为是类的静态变量,必须有一个通用的声明  
  16.  template<typename T> typename Singleton_2B::object_type  Singleton_2B::instance_;  
  17.    
  18.  //测试的例子代码。  
  19.  class Object_2B_1  
  20.  {  
  21.        
  22.      //其实使用友元帮助我们可以让Object_2B的构造函数是protected的,从而真正实现单子的意图  
  23.      friend class Singleton_2B;  
  24.      //注意下面用protected,大家无法构造实例  
  25.  protected:  
  26.      Object_2B_1();  
  27.      ~Object_2B_1(){};  
  28.  public:  
  29.      void do_something();  
  30.  protected:  
  31.      int data_2b_1_;  
  32.  };  
  33.    
  34.  class Object_2B_2  
  35.  {  
  36.      friend class Singleton_2B;  
  37.  protected:  
  38.      Object_2B_2();  
  39.      ~Object_2B_2(){};  
  40.  public:  
  41.      void do_something();  
  42.  protected:  
  43.      int data_2b_2_;  
  44.  };  
  45.    
  46.  //CPP文件  
  47.  Object_2B_1::Object_2B_1():  
  48.      data_2b_1_(1)  
  49.  {  
  50.      printf("Object_2B_1::Object_2B_1() this:[%p] data_2b_1_ [%d].\n",this,data_2b_1_);  
  51.      Singleton_2B::instance()->do_something();      
  52.  };  
  53.    
  54.  void Object_2B_1::do_something()  
  55.  {  
  56.      data_2b_1_+= 10000;  
  57.      printf("Object_2B_1::do_something() this:[%p] data_2b_1_ [%d].\n",this,data_2b_1_);  
  58.        
  59.  }  
  60.    
  61.    
  62.  Object_2B_2::Object_2B_2():  
  63.          data_2b_2_(2)  
  64.  {  
  65.      printf("Object_2B_2::Object_2B_2() this:[%p] data_2b_2_ [%d].\n",this,data_2b_2_);  
  66.      Singleton_2B::instance()->do_something();  
  67.  };  
  68.    
  69.  void Object_2B_2::do_something()  
  70.  {  
  71.      data_2b_2_+= 10000;  
  72.      printf("Object_2B_2::do_something() this:[%p] data_2b_2_ [%d].\n",this,data_2b_2_);  
  73.  }  


如代码:Singleton_2B是一个singleton的模板封装,根据这个模版,我们实现了2个单子类,Object_2B_1和Object_2B_2,为了模仿问题,我们在其各自的构造函数里面又都调用了其他一个对象的instance函数。因为我们知道,全局和类static 变量的初始化是编译器自己控制的,我们无法干涉,所以如果假设Object_2B_1::instance_静态成员变量被先构造,他的构造函数调用里面调用Object_2B_2::instance().do_something()函数时,Object_2B_2::instance_可能还没有构造出来。从而导致问题。

但会导致什么问题呢?崩溃?不一定是,(因为静态数据区的空间应该是先分配的),而且结果这个和编译器的实现有关系,

GCC的输出结果如下:

[cpp]  view plain  copy
  1. //GCC编译器的输出如下:  
  2. Object_2B_2::Object_2B_2() this:[0046E1F4] data_2b_2_ [2].  
  3. //注意下面,do_something函数被调用了,但是没有崩溃,data_2b_1_默认被初始化为了0  
  4. Object_2B_1::do_something() this:[0046E1F0] data_2b_1_ [10000].    
  5. //注意下面,do_something函数被调用了后,构造函数才起作用,data_2b_1_又被初始化为了1,  
  6. Object_2B_1::Object_2B_1() this:[0046E1F0] data_2b_1_ [1].   
  7. Object_2B_2::do_something() this:[0046E1F4] data_2b_2_ [10002].  

VS2010的DEBUG版本的输出和GCC一致,但有意思的是Realse版本输出结果如下:

[cpp]  view plain  copy
  1. //VC++2010的release版本输出   
  2. Object_2B_2::Object_2B_2() this:[0046E1F4] data_2b_2_ [2].  
  3. //注意下面的10001,感觉就像构造函数被偷偷调用过一样。有意思。  
  4. Object_2B_1::do_something() this:[0046E1F0] data_2b_1_ [10001].    
  5. Object_2B_1::Object_2B_1() this:[0046E1F0] data_2b_1_ [1].   
  6. Object_2B_2::do_something() this:[0046E1F4] data_2b_2_ [10002].  


 

2          BOOST的实现如何规避问题

接着我们就来看看BOOST的模版是使用什么技巧,即保证多线程下不重复初始化,又让相互之间的调用更加安全。

[cpp]  view plain  copy
  1. template <typename T>  
  2. class Singleton_WY  
  3. {  
  4. private:  
  5.     struct object_creator  
  6.     {  
  7.         object_creator()   
  8.         {  
  9.             Singleton_WY::instance();   
  10.         }  
  11.         inline void do_nothing() const {}  
  12.     };  
  13.     //利用类的静态对象object_creator的构造初始化,在进入main之前已经调用了instance  
  14.     //从而避免了多次初始化的问题  
  15.     static object_creator create_object_;  
  16. public:  
  17.     static T *instance()  
  18.     {  
  19.         static T obj;  
  20.         //do_nothing 是必要的,do_nothing的作用有点意思,  
  21.         //如果不加create_object_.do_nothing();这句话,在main函数前面  
  22.         //create_object_的构造函数都不会被调用,instance当然也不会被调用,  
  23.         //我的估计是模版的延迟实现的特效导致,如果没有这句话,编译器也不会实现  
  24.     // Singleton_WY::object_creator,所以就会导致这个问题  
  25.         create_object_.do_nothing();  
  26.         return &obj;  
  27.     }  
  28. };  
  29. //因为create_object_是类的静态变量,必须有一个通用的声明  
  30. template <typename T>  typename Singleton_WY::object_creator Singleton_WY::create_object_;  
  31.   
  32. //测试的例子  
  33. class Object_WY_1  
  34. {  
  35.     //其实使用友元帮助我们可以让Object_2B的构造函数是protected的,从而真正实现单子的意图  
  36.     friend class Singleton_WY;  
  37.     //注意下面用protected,大家无法构造实例  
  38. protected:  
  39.     Object_WY_1();  
  40.     ~Object_WY_1(){};  
  41. public:  
  42.     void do_something();  
  43. protected:  
  44.     int data_wy_1_;  
  45. };  
  46.   
  47. class Object_WY_2  
  48. {  
  49.     friend class Singleton_WY;  
  50. protected:  
  51.     Object_WY_2();  
  52.     ~Object_WY_2(){};  
  53. public:  
  54.     void do_something();  
  55. protected:  
  56.     int data_wy_2_;  
  57. };  
  58.   
  59. //CPP代码  
  60. Object_WY_1::Object_WY_1():  
  61.     data_wy_1_(1)  
  62. {  
  63.     printf("Object_WY_1::Object_WY_1() this:[%p] data_2b_1_ [%d].\n",this,data_wy_1_);  
  64.     Singleton_WY::instance()->do_something();      
  65. };  
  66.   
  67. void Object_WY_1::do_something()  
  68. {  
  69.     data_wy_1_+= 10000;  
  70.     printf("Object_2B_1::do_something() this:[%p] data_2b_1_ [%d].\n",this,data_wy_1_);  
  71.   
  72. }  
  73.   
  74.   
  75. Object_WY_2::Object_WY_2():  
  76.     data_wy_2_(2)  
  77. {  
  78.     printf("Object_WY_2::Object_WY_2() this:[%p] data_2b_2_ [%d].\n",this,data_wy_2_);  
  79.     Singleton_WY::instance()->do_something();  
  80. };  
  81.   
  82. void Object_WY_2::do_something()  
  83. {  
  84.     data_wy_2_+= 10000;  
  85.     printf("Object_WY_2::do_something() this:[%p] data_2b_2_ [%d].\n",this,data_wy_2_);  
  86. }  



调用输出的结果如下,大家可以发现调用顺序正确了Object_WY_2的构造函数内部调用:Singleton_WY::instance()函数的时候,Object_WY_1的单子实例就被创建出来了。
[cpp]  view plain  copy
  1. Object_WY_2::Object_WY_2() this:[00ECA138] data_2b_2_ [2].  
  2. Object_WY_1::Object_WY_1() this:[00ECA140] data_2b_1_ [1].  
  3. Object_WY_2::do_something() this:[00ECA138] data_2b_2_ [10002].  
  4. Object_2B_1::do_something() this:[00ECA140] data_2b_1_ [10001].  

首先BOOST的这个实现的Singleton的数据分成两个部分,一个是内部类的object_creator的静态成员creator_object_,一个是instance函数内部的静态变量static T obj;如果外部的有人调用了instance()函数,静态变量obj就会被构造出来,而静态成员creator_object_会在main函数前面构造,他的构造函数内部也调用instance(),这样就会保证静态变量一定会在main函数前面初始化出来。

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

create_object_.do_nothing();

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

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

3          个人感觉

也许是因为我本身对Singleton的模版就不感冒,我对文艺青年的这个Singleton也没有太大胃口,

一方面是技巧性过强,我不才,do_nothing()那句话的问题我研究了半天。

二是由于他将所有的instance初始化放在了main函数前面,好处是避免了多线程多次初始化的麻烦,但也限制了初始化的多样性。一些太强的逻辑关系的情况下这招并不好。

三是这种依靠类static变量的方式,无法按需启动,回收。

四是性能,每次do_nothine也是无谓的消耗呀。

为了一个很简单的风险(多次初始化),引入一个技巧性很强的又有各种限制的东东。是否有点画蛇添足。YY。


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