分类: C/C++
2009-11-25 18:50:58
一、在GOF著作中对Singleton模式的实现方式如下:
- /*解一*/
- // Header file Singleton.h
- class Singleton
- {
- public:
- static Singleton *Instance(){ //1
- if( !m_pInstatnce) //2
- m_pInstance = new Singleton;//3
- return m_pInstance; //4
- }
- void DoSomething();
- private:
- static Singleton *m_pInstatnce=NULL; //5
- private:
- Singleton(); //6
- Singleton(const Singleton&); //7
- Singleton& operator=(const Singleton&); //8
- ~Singleton(); //9
- }
- // Implementation file Singleton.cpp
- Singleton* Singleton::m_pInstance = 0;
客户代码现在可以这样使用Singleton:
在上面的解决方案中,我们只在需要调用时,才产生一个Singleton的对象。这样带来的好处是,
如果该对象产生带来的结果很昂贵,但不经常用到时,是一种非常好的策略。
但如果该Instance被频繁调用,就有人觉得Instance()中的判断降低了效率。
二、Meyers Singleton
我们如何解决这个问题呢,实际上很简单。一种非常优雅的做法由Scott Meyers最先提出,故也称为Meyers Singleton。它依赖编译器的神奇技巧,即函数内的static对象只在该函数第一次执行时才初始化(请注意不是static常量)。
// Header file Singleton.h
解二在Instance中定义了一个Static的Singleton对象,来解决Instance中初始化的问题,也很顺利的解决了定义Static成员对象带来的问题。
请注意,解二在VC6中不能编译通过,将有以下的错误:
产生该问题的错误原因是什么呢(请仔细思考^_^)
原因在于在产生static Singleton对象后,编译器会自动产生一个销毁函数__DestroySingleton(),然后调用atexit()注册,在程序退出时执行 __DestroySingleton。但由于Singleton的析构函数是private,所以会产生访问错误。(应该在以后的编译器中修改了该 BUG)
三、应用于多线程
解一和解二都不能用于多线程,要想用于多线程,还得引入锁机制。
将解一的Instance()改为如下:
此种方法将解决解一运行在多线程环境下内存泄漏的问题,但带来的结果是,当m_mutex被锁定时,其它试图锁定m_mutex的线程都将必须等等。并且每次执行锁操作其付出的代价极大,亦即是这种方案的解决办法并不吸引人。
那么我们将上面的代码改为如下方式:
这样修改的结果没有问题了么?NO!!!!该方案带来的结果同解一,原因也一样,都将造成内存泄漏。此时“双检测锁定”模式就粉墨登场了。
由Doug Schmidt和Tim Harrison提出了“双检测锁定”(Double-Checked Locking)模式来解决multithread singletons问题。
上面的方案就完美了么。回答还是NO!!!(各位看官是否已经郁闷了啊,这不是玩我啊?请耐心点,听我细细到来^_^)
如果在RISC机器上编译器有可能将上面的代码优化,在锁定m_mutex前执行第3句。这是完全有可能的,因为第一句和第3句一样,根据代码优化原则是可以这样处理的。这样一来,我们引以为自豪的“双检测锁定”居然没有起作用( L)
怎么办?解决呗。怎么解决?简单,我们在m_pInstance前面加一个修饰符就可以了。什么修饰符呢?……
volatile(简单吧)
那么我们完整的解法如下:
// Header file Singleton.h
// Implementation file Singleton.cpp
四、Singleton销毁
在这里,我们就到了Singleton最简单也最复杂的地方了。
为什么说它简单?我们根本可以不理睬创建的对象m_pInstance的销毁啊。因为虽然我们一直没有将Singleton对象删除,但不会造成内 存泄漏。为什么这样说呢?因为只有当你分配了积累性数据并丢失了对他的所有reference时,内存泄漏才发生。而对Singleton并不属于上面的 情况,没有累积性的东东,而且直到结束我们还有它的引用。在现代操作系统中,当一个进程结束后,将自动将该进程所有内存空间完全释放。(可以参考 《effective C++》条款10,里面讲述了内存泄漏)。
但有时泄漏还是存在的,那是什么呢?就是资源泄漏(resource leak)。比如说如果该Singleton对象管理的是网络连接,OS互斥量,进程通信的handles等等。这时我们就必须考虑到Singleton的销毁了。
解决方法是引入smart pointer
唯一修正 resource leak的方法就是在程序结束的时候delete _instance。当然了,用smart
pointer再好不过,在这里用auto_ptr就可以满足需要了(如果你还不知道smart_ptr是什么,花点时间熟悉C++标准库吧),修改后的
代码如下:
五、不死鸟模式(Phoenix Singleton)
我们以KDL(keyboard,display,log)模型为例,其中K,D,L均使用Singleton模式。只要keyboard或者 display出现异常,我们就必须调用log将其写入日志中,否则log对象不应该创建。对后面一条,我们的Singleton创建时就可以满足。
在前面我们已经说到,在产生一个对象时(非用new产生的对象),由编译器自动调用了atexit(__DestroyObject)函数来实现该对象的析构操作。而C++对象析构是LIFO进行的,即先产生的对象后摧毁。
如果在一般情况下调用了log对象,然后开始销毁对象。按照“后创建的先销毁”原则:log对象将被销毁,然后display对象开始销毁。此时 display在销毁发现出现异常,于是调用log对象进行记录。但事实上,log对象已经被销毁,那么调用log对象将产生不可预期的后果,此问题我们 称为Dead Reference。所以前面的解决方案不能解决目前我们遇到的问题。
Andrei Alexandrescu提出了解决方案,称为Phoenix Singleton。
修改后的 代码如下:
对于不死鸟模式还有一种编程方式,目前理解还不太清楚,先记下来再说:
请注意此处OnDeadReference()中所使用的new操作符的用法:是所谓的placement new操作,它并不分配内存,而是在某个地址上构造一个新对象。