探讨单件模式(1)

有关设计模式的东西很早就听说过一些了,但因为以前的开发经历中对设计模式很少有直接的需求,所以也没花太多心思研究过.前不久公司组织Training,内容是Design Pattern的Singleton(单件模式).原本以为Singleton是一个很simple的模式,这个培训应该也不会有太深入的内容,不过想想自己毕竟没有系统地研究过设计模式,所以还是去参加了,听完training以后才发现原来一个Singleton里面会包括这么丰富的内容,这实在是自己始料不及的.

写出一个基本的Singleton的例子程序是很简单的,有一些基本的编程基础和Object Oriented的概念,读一下有关Singleton的介绍就可以写出来类似于下面的程序.


例子 1:
class SingletonExample {
 private:
   SingletonExample()
   {
   ......
  }
  SingletonExample()
  {
   ......
  }
  SingletonExample * Instance( void )
  {
   return &singleInst;
  }
 private:
  static SingletonExample singleInst;  
};
SingletonExample SingletonExample::singleInst;


这个例子程序虽然不完善, 但基本上体现了Singleton的风貌.每次调用Instance接口都会返回唯一的SingletonExample对象.

但是Singleton是否就止步于此了?

分析一下这个例子程序中可能存在的问题:

无论SingletonExample类的Instance是否会被调用,都会为SingletonExample的静态对象分配内存空间,这就带来无谓的内存消耗.

针对这一点问题我们可能会直观地想到作出如下改进:

例子 2:
class SingletonExample {
 private:
  SingletonExample()
  {
   ......
  }
  ~SingletonExample()
  {
   
  }
 public:
  SingletonExample * Instance(void)
  {
   static SingletonExample singInst;
   return &singInst;
  }
};
为了便于理解,将编译器为这个class可能生成的实际目标码描述如下:

class SingletonExample {
 private:
  SingletonExample()
  {
   ......
  }
  ~SingletonExample()
  {
   
  }
 public:
  SingletonExample * Instance(void)
  {
   static SingletonExample singleInst;
   static bool objInited = false;  // 编译器生成的内部变量
   
   if ( !objInited ) { // 编译器插入的代码,用于完成函数内部静态对象的初始化工作
    objInited = true;
    singleInst.SingletonExample();
    atexit( singleInst.~SingletonExample() ); // 将针对singleInst对象的destructor注册到
                                                                             //程序退出会执行的函数链中,以确保静态对象在程序退
                                                                             //出时会被析构.
                                                                             // 注: 不同的平台下对于函数内部的静态对象
                                                                             //的destructor的调用手段可能会有所不同,这里
                                                                             //只是给出了Linux平台下使用g++可能生成
                                                                             //的目标码!
   }
  
   return &singleInst;
  }
};

与例子1相比,由于 singleInst 是Instance()方法内部的一个静态对象,所以只有在Instance()被调用到时才会
为singleInst对象分配内存资源,这就解决了例子 1 中的问题.

但是例子2是不是就完美地实现了单件模式了呢?

例子2中可能存在Dead Reference 的问题!所谓Dead Reference,简单来说,就是在程序中调用 SingletonExample::Instance()返回
SingletonExample对象的指针时,这个指针指向的是一个已经被析构函数释放了的无效对象!

下面给出一个具体的场景来描述Dead Reference Problem.

现在有两个采用单件模式实现的类:

Logger:日志管理类,用来记录程序运行过程中的日志信息

ErrorHandling: 错误管理类,用来输出错误信息.

在ErrorHandling的析构函数内部会调用Logger将错误信息写入日志文件中.

现在有一个应用程序中使用到了Logger和ErrorHandling,在程序的初始化过程中,试图分配1个G的内存,但内存申请失败,于是程序调用exit()退出,由于Logger和ErrorHandling的Destructor均是通过atexit()完成注册的,因此,Logger和ErrorHandling的单件对象均会被析构,如果Logger在ErrorHandling之后析构,一切正常,但如果在ErrorHandling析构之前Logger就已经被析构了,由于ErrorHandling的析构函数中会访问Logger单件对象时,Dead Reference Problem就产生了!

怎样解决 Dead Reference Problem呢?一个不算优雅但比较直接的方式如例子4所示.


例子 3:
class SingletonExample {
 private:
  SingletonExample()
  {
   ......
  }
  ~SingletonExample()
  {
   // This destructor would never be called!
  }
 public:
  SingletonExample * Instance(void)
  {
   if (NULL == pInstance) {
    pInstance = new Singleton;
   }
   return pInstance;
  }
 private:
  static SingletonExample * pInstance;  
};


可以注意到例子3中SingletonExample的单件对象对应的只有一个new操作符,没有delete操作符,所以这个单件对象不会被释放,因此Dead Reference Problem不会出现.但例子3的解决方案也并非完美.


例3最主要的问题是:
 首先,new操作没有对应的delete操作.这不是一种良好的编程风范.
 而更重要的问题则是虽然由于单件模式保证了对于Singleton的Instance接口来说,在应用程序的生存期限里只会有一份SingletonExample对象存在,不会有memory leak存在.但是由于Instance()内部调用new操作符所分配出来的对象不会执行destructor,会存在资源泄漏的隐患.想像一下在Singleton对象的构造函数内部执行了一些申请操作系统资源(诸如信号量,网络套接字之类)的动作,但相应的释放操作却不会被执行,这样就很可能在应用程序退出时带来系统资源的泄漏.

可以看出一个看似简单的Singleton模式在具体实现中会涉及到如此多的问题,说设计模式是前人智慧的结晶实在是此言不虚,关于Singleton模式的进一步探讨会在后面的blog中继续.

你可能感兴趣的:(探讨单件模式(1))