之前一直徘徊第一篇该写哪一种设计模式,最后决定还是以Singleton模式开始吧。之所以以它开始,原因在我于个人认为,相对来说它在设计上比较单一,比较简单一些。在通常情况下,它是最容易理解的。同样也正因为它容易理解,细节才更值得注意,越是简单的东西,往往会被我们忽略一些细节。关于Singleton模式的讨论和实现也非常的多,GOF设计模式也只对它进行了简单的描述,本文则打算相对全面的介绍一下Singleton模式,目的在于挖掘设计的思想。
Singleton模式又称单件模式,我们都知道全局变量,Singleton就相当于一个全局变量,只不过它是一种被改进的全局变量。体现在哪儿呢?普通的全局变量可以有多个,例如某个类,可以定义很多个这个类的全局对象,可以分别服务于整个应用程序的各个模块。而Singleton则只允许创建一个对象,就好比不允许有克隆人一样,哪天在大街上看见一个一模一样的你,你会是什么感受?从计算机的角度,例如键盘、显示器、系统时钟等,都应该是Singleton,如果有多个这样的对象存在,很可能带来危险,而这些危险却不能换来切实的好处。
在GOF设计模式里,对Singleton的描述很简单:“保证一个类只有一个实体(instance),并为其提供一个全局访问点(global access point)”。所谓全局访问点,就是Singleton类的一个公共全局的访问接口,用于获得Singleton实体,对于用户来说,只需要简单的步骤就能获得这个实体,与全局变量的访问一样简单,而Singleton对象的创建与销毁有Singleton类自己承担,用户不必操心。既然是自己管理,我们就得将其管理好,让用户放心,现在是提倡服务质量的社会,我们应该有服务的态度。
理论讲了一大堆,迫不及待想看看具体实现,先看一个初期的版本:
/* Singleton.h */
//-----------------------------------------------------------------------------------
// Desc: Singleton Header File
// Author: masefee
// Date: 2010.09
// Copyright (C) 2010 masefee
//-----------------------------------------------------------------------------------
#ifndef __SINGLETON_H__
#define __SINGLETON_H__
class Singleton
{
public:
static Singleton* Instance( void );
public:
int doSomething( void );
protected:
Singleton( void ){}
private:
static Singleton* ms_pInstance;
};
#endif
/* Singleton.cpp */
//-----------------------------------------------------------------------------------
// Desc: Singleton Source File
// Author: masefee
// Date: 2010.09
// Copyright (C) 2010 masefee
//-----------------------------------------------------------------------------------
#include "Singleton.h"
Singleton* Singleton::ms_pInstance = 0;
Singleton* Singleton::Instance( void )
{
if ( ms_pInstance == 0 )
ms_pInstance = new Singleton;
return ms_pInstance;
}
int Singleton::doSomething( void )
{
}
/* main.cpp */
#include "Singleton.h"
int main( void )
{
Singleton* sig = Singleton::Instance();
sig->doSomething();
delete sig;
return 0;
}
Singleton类的构造函数被声明为protected,当然也可以声明为private,声明为protected是为了能够被继承。但用户不能自己产生Singleton对象,唯一能产生Singleton对象的就只有Instance成员函数,这个Instance函数即所谓的全局访问点。访问方式如上面main函数中的红色代码。如果用户没有调用Instance函数,Singleton对象就不会产生出来,这样优化的成本是Instance函数里的if检测,但是好处是,如果Singleton对象产生很昂贵,而本身有很少使用,这种“使用才诞生”的方案就会显尽优势了。
上面将ms_pInstance在全局初始化为0,这样做有个好处,即编译器在编译时就已经将ms_pInstance的初始值写到了可执行二进制文件里了,Singleton对象的唯一性在这个时期就已经决定了,这也正是C++实现Singleton模式的精髓所在。如果将ms_pInstance改造一下,如:
class Singleton
{
public:
static Singleton* Instance( void );
public:
void doSomething( void );
protected:
Singleton( void ){}
private:
static Singleton ms_Instance;
};
Singleton Singleton::ms_Instance;
Singleton* Singleton::Instance( void )
{
return &ms_Instance;
}
将ms_pInstance由指针改成了对象,这样做未必是件好事,原因在于ms_Instance是被动态初始化(在程序运行期间调用构造函数进行初始化)的,而ms_pInstance在前面已经说过,它是属于静态初始化,编译器在编译时就将常量写入到二进制文件里,在程序装载到内存时,就会被初始化。我们都知道,在进入main之前,有很多初始化操作,对于不同编译单元的动态初始化对象,C++并没有规定其初始化顺序,因此上面的改造方法存在如下隐患:
#include "Singleton.h"
int g_iRetVal = Singleton::Instance()->doSomething();
由于无法确保编译器一定先将ms_Instance对象初始化,所以全局变量g_iRetVal在被初始赋值时,Singleton::Instance()调用可能返回一个尚未构造的对象,这也就意味着你无法保证任何外部的对象使用的ms_Instance对象都是一个已经被正确初始化的对象。危险也就不言而喻了。
Singleton模式意为单件模式,于是保证其唯一性就成了关键,看看上面的第一种实现,ms_pInstance虽然是Instance成员函数所创建,它返回了一个Singleton对象的指针给外界,并且将这个指针的销毁权利赋予了外界,这就存在了第一个隐患,倘若外界将返回的指针给销毁了,然后再重新调用Instance函数,则前后对象内存地址通常将发生变化,如:
Singleton* sig1 = Singleton::Instance();
sig->doSomething();
delete sig;
Singleton* sig2 = Singleton::Instance();
如上,sig1和sig2的指向的Singleton对象的内存地址通常是不一样的,例如前面的sig1指向的对象保存了一些状态,这样销毁之后再次创建,状态已经被清除,程序也就容易出错了。所以为了避免这样类似的情况发生,我们再改进一下Instance函数:
static Singleton& Instance( void );
传回引用则不用担心被用户释放掉对象了,这样就比较安全了,Singleton对象都由其自身管理。
在C++类中,还有一个copy(复制)构造函数,在上面的Singleton类里,我们并没有显示声明copy构造,于是如果有以下写法:
Singleton sig( Singleton::Instance() );
如果我们不显示声明一个copy构造,编译器会帮你生成一个默认的public版本的copy构造。上面的写法就会调用默认的copy构造,从而用户就能在外部声明一个Singleton对象了,这样就存在了第二个隐患。因此,我们将copy构造也声明为protected保护成员。
另外还有一个成员函数,赋值(assignment)操作符。因为你不能将一个Singleton对象赋值给另外一个Singleton对象,这违背了唯一性,不允许存在两个Singleton对象。因此我们将赋值操作符也声明为保护成员,同时对于Singleton来说,它的赋值没有意义,唯一性的原则就使它只能赋值给自己,所以赋值操作符我们不用去具体实现。
最后一个是析构函数,如前面所说,用户会在外界释放掉Singleton对象,为了避免这一点,所以我们也将析构函数声明为保护成员,就不会意外被释放了。
上述所有手段统一到一起之后,Singleton类的接口声明如下:
class Singleton
{
public:
static Singleton& Instance( void );
public:
void doSomething( void );
protected:
Singleton( void );
Singleton( const Singleton& other );
~Singleton( void );
Singleton& operator =( const Singleton& other );
private:
static Singleton* ms_pInstance;
};
这样似乎已经完美了,Singleton对象的创建完全有Singleton类自身负责了,再看前面的创建过程,ms_pInstance是一个指针,Singleton对象是动态分配(new)出来的,那么释放过程就得我们手工调用delete,否则将发生内存泄露。然而析构函数又被我们定义为保护成员了,因此析构问题还没有得到解决。
这成了一个比较棘手的问题,既要保证程序运行时整个范围的唯一性,又要保证在销毁Singleton对象时没有人在使用它,所以销毁的时机显得尤为重要,也比较难把握。
于是有人想到了一个比较简单的方案,不动态分配Singleton对象便可以自动销毁了,但销毁的最好时期是在程序结束时最好,于是想到了如下方案:
Singleton& Singleton::Instance( void )
{
static Singleton _inst;
return _inst;
}
_inst是一个静态的局部变量,它的初始化是在第一次进入Instance函数时,这属于执行期初始化,而与编译期间常量初始化不同,_inst对象初始化要调用构造函数,这不可能在编译期间完成,与:
int func( void )
{
static int a = 100; // 编译期间常量初始化
return a;
}
不同。a的值在编译期间就已经决定了,在应用程序装载到内存时就已经为100了,而非在第一次执行func函数时才被赋值为100。
_inst对象的销毁工作由编译器承担,编译器将_inst对象的销毁过程注册到atexit,它是一个标准C语言库函数,让你注册一些函数得以在程序结束时调用,调用次序与栈操作类似,后进先出的原则。atexit的原型:
int __cdecl atexit( void ( __cdecl* pFunc )( void ) );
在Instance函数的反汇编代码上有所体现(VS2008 Release 禁用优化(/Od)):
Singleton& Singleton::Instance( void )
{
00CB1040 push ebp
00CB1041 mov ebp,esp
static Singleton _inst;
00CB1043 mov eax,dword ptr [$S1 (0CB3374h)]
00CB1048 and eax,1
00CB104B jne Singleton::Instance+33h (0CB1073h)
00CB104D mov ecx,dword ptr [$S1 (0CB3374h)]
00CB1053 or ecx,1
00CB1056 mov dword ptr [$S1 (0CB3374h)],ecx
00CB105C mov ecx,offset _inst (0CB3370h)
00CB1061 call Singleton::Singleton (0CB1020h)
00CB1066 push offset `Singleton::Instance'::`2'::`dynamic atexit destructor for '_inst'' (0CB1880h)
00CB106B call atexit (0CB112Eh)
00CB1070 add esp,4
return _inst;
00CB1073 mov eax,offset _inst (0CB3370h)
}
00CB1078 pop ebp
00CB1079 ret
红色的一句汇编代码即是得到_inst的析构过程地址压入到atexit的参数列表,红色粗体则调用了atexit函数注册这个析构过程。这里所谓的析构过程并不是Singleton类的析构函数,而是如下过程:
`Singleton::Instance'::`2'::`dynamic atexit destructor for '_inst'':
00CB1880 push ebp
00CB1881 mov ebp,esp
00CB1883 mov ecx,offset _inst (0CB3370h)
00CB1888 call Singleton::~Singleton (0CB1030h)
00CB188D pop ebp
00CB188E ret
这也是一个函数,在此函数里再调用Singleton的析构函数,如蓝色那句汇编代码。道理很简单,由于Singleton的析构函数是__thiscall,需要传递类对象,所以不是直接call析构函数的地址。
这种方式销毁在大多数情况下是有效的,在实际中,这种方式也用得比较多。可以根据实际的情况,选择不同的机制,Singleton没有定死只能用哪种方式。
既然上述方式在大多数情况下是有效的,那么肯定就有一些情况会有问题,这就引出了KDL(keyboard、display、log)问题,假设我们程序中有三个singletons:keyboard、display、log,keyboard和display表示真实的物体,log表示日志记录,可以是输出到屏幕或者记录到文件。而且log由于创建过程有一定的开销,因此在有错误时才会被创建,如果程序一直没有错误,则log将不会被创建。
假如程序开始执行,keyboard顺利创建成功,而display创建过程中出现错误,这是需要产生一条log记录,log也就被创建了。这时由于display创建失败了,程序需要退出,由于atexit是后注册的先调用,log最后创建,则也是最后注册atexit的,因此log最先销毁,这没有问题。但是log销毁了,如果随后的keyboard如果销毁失败需要产生一条log记录,而这是log已经销毁了,log::Instance会不明事理的返回一个引用,指向了一个log对象的空壳,此后程序便不能确定其行为了,很可能发生其他的错误,这也称之为"dead - reference"问题。
从上面的分析来看,我们是想要log最后销毁,不管它是在什么时候创建的,都得在keyboard和display之后销毁,这样才能记录它们的析构过程中发生的错误。于是我们又想到,可以通过记录一个状态,来作为"dead - reference"检测。例如定义一个static bool ms_bDestroyed变量来标记Singleton是否已经被销毁。如果已经销毁则置为true,反之置为false。
/* Singleton.h */
class Singleton
{
public:
static Singleton& Instance( void );
public:
void doSomething( void );
protected:
Singleton( void ){};
Singleton( const Singleton& other );
~Singleton( void );
Singleton& operator =( const Singleton& other );
private:
static Singleton* ms_pInstance;
static bool ms_bDestroyed;
};
/* Singleton.cpp */
#include <iostream>
#include "Singleton.h"
Singleton* Singleton::ms_pInstance = 0;
bool Singleton::ms_bDestroyed = false;
Singleton& Singleton::Instance( void )
{
if ( !ms_pInstance )
{
if ( ms_bDestroyed )
throw std::runtime_error( "Dead Reference Detected" );
else
{
static Singleton _inst;
ms_pInstance = &_inst;
}
}
return *ms_pInstance;
}
Singleton::~Singleton( void )
{
ms_pInstance = 0;
ms_bDestroyed = true;
}
void Singleton::doSomething( void )
{
}
这种方案能够准确的检测"dead - reference",如果Singleton已经被销毁,ms_bDestroyed成员被置为true,再次获取Singleton对象时,则会抛出一个std::runtime_error异常,避免程序存在不确定行为。这种方案相对来说比较高效简洁了,也可适用于一定场合。
但这种方案在有的时候也不能让我们满意,虽然抛出了异常,但是KDL问题还是没有被最终解决,只是规避了不确定行为。于是我们又想到了一种方案,即是让log重生,一旦发现log被销毁了,而又需要记录log,则再次创建log。这就能保证log至始至终一直存在了,我们只需要在Singleton类里添加一个新的成员函数:
class Singleton
{
... ... other member ... ...
private:
static void destroySingleton( void );
};
实现则为:
void Singleton::destroySingleton( void )
{
ms_pInstance->~Singleton();
}
Instance成员就得在改造一下了:
Singleton& Singleton::Instance( void )
{
if ( !ms_pInstance )
{
static Singleton _inst;
ms_pInstance = &_inst;
if ( ms_bDestroyed )
{
new( ms_pInstance ) Singleton;
atexit( destroySingleton );
ms_bDestroyed = false;
}
}
return *ms_pInstance;
}
destroySingleton与前面汇编那段相似,相当于这个工作让我们自己来做了,而不是让编译器来做。destroySingleton手动调用Singleton的析构函数,而destroySingleton又被我们注册到atexit。当ms_pDestroyed为真时,则再次在ms_pInstance指向的内存出创建一个新的Singleton对象,这里使用的是placement new操作符,它并不会新开辟内存(参见:利用C++的operator new实现同一对象多次调用构造函数)。之后,注册destroySingleton为atexit,在程序结束时调用并析构Singleton对象。如此而来,便保证了Singleton对象的生命期跨越整个应用程序。log如果作为这样一个Singleton对象,那么无论log在什么时候被销毁,都能记录所有错误日志了。
似乎到此已经就非常完美了,但是还有一点不得不提,使用atexit具有一个未定义行为:
void func1( void )
{
}
void func2( void )
{
atexit( func1 );
}
void func3( void )
{
}
int main( void )
{
atexit( func2 );
atexit( func3 );
return 0;
}
CC++标准里并没有规定上面这种情况的执行次序,按前面的后注册先执行的说法,按理说func1被最后注册,则应该最先执行它,但是它的注册是由func2负责的,这就得先执行func2,才能注册func1。这样就产生了矛盾,所以以编译器的次序为准,在VS2008下,上面的例子中,最先执行func3,再执行func2,最后执行func1。看起来像是一个层级关系,main函数的注册顺序是一层,后来的又是一层,也可以认为在被注册为atexit的函数里再注册其它函数时,其它函数的执行次序在当前函数之后,如果注册了多个,则最后注册的在当前函数执行后立即执行。
上面这种机制是通过延长Singleton的声明周期,它破坏了其正常的生命周期。很可能带来不必要的迷惑,于是我们又想到了一种机制:是否能够控制Singleton的寿命呢,让log的寿命比keyboard和display的寿命长,便能够解决KDL问题了。控制寿命还可以针对不同的对象,不单单只是Singleton,它可以说是一种可以移植的概念。
我们实现一个生命期管理器,如下:
- ---LifeTime.hpp--
- --由于代码编辑器的问题,省略了include--
- class BaseLifetimeTracker
- {
- public:
- BaseLifetimeTracker( unsigned int longevity )
- : m_iLongevity( longevity )
- {
- }
- virtual ~BaseLifetimeTracker( void )
- {
- }
- friend inline bool Compare( const BaseLifetimeTracker* x,
- const BaseLifetimeTracker* y )
- {
- return y->m_iLongevity < x->m_iLongevity;
- }
- protected:
- static unsigned int elements;
- static BaseLifetimeTracker** pTrackerArray;
- private:
- unsigned int m_iLongevity;
- };
- unsigned int BaseLifetimeTracker::elements = 0;
- BaseLifetimeTracker** BaseLifetimeTracker::pTrackerArray = 0;
- template< class T >
- class Deleter
- {
- public:
- static void Delete( T* pObj )
- {
- delete pObj;
- }
- };
- template< class T, class Destroyer = Deleter< T > >
- class LifetimeTracker :
- public BaseLifetimeTracker
- {
- public:
- static void SetLongevity( T* pDynObj, unsigned int longenvity )
- {
- BaseLifetimeTracker** pNewArray = static_cast< BaseLifetimeTracker** >(
- std::realloc( pTrackerArray, sizeof( BaseLifetimeTracker* ) * ( elements + 1 ) ) );
- if ( !pNewArray )
- throw std::bad_alloc();
- pTrackerArray = pNewArray;
- BaseLifetimeTracker* pInsert = new LifetimeTracker< T >( pDynObj, longenvity );
- BaseLifetimeTracker** pos = std::upper_bound( pTrackerArray, pTrackerArray + elements, pInsert, Compare );
- std::copy_backward( pos, pTrackerArray + elements, pTrackerArray + elements + 1 );
- *pos = pInsert;
- ++elements;
- std::atexit( AtExitFn );
- }
- static void AtExitFn( void )
- {
- assert( elements > 0 && pTrackerArray != 0 );
- BaseLifetimeTracker* pTop = pTrackerArray[ elements - 1 ];
- pTrackerArray = static_cast< BaseLifetimeTracker** >(
- std::realloc( pTrackerArray, sizeof( BaseLifetimeTracker* ) * --elements ) );
- delete pTop;
- }
- public:
- LifetimeTracker( T* pDynObj, unsigned int longevity )
- : BaseLifetimeTracker( longevity ), m_pTracked( pDynObj )
- {
- }
- ~LifetimeTracker( void )
- {
- Destroyer::Delete( m_pTracked );
- }
- private:
- T* m_pTracked;
- };
有了这个管理器,就能设置Singleton对象的寿命了,pTrackerArray是按生命周期的长度进行升序排列的,最前面的就是最先销毁的,这与前面的销毁规则是一致的。 对于前面的KDL问题,我们就可以将keyboard和display的寿命设置为1,log的寿命设置为2,keyboard和display不存在先后问题,寿命相同也不影响。log寿命为2,大于keyboard和display就行,保证在最后销毁。这样一来,在单个线程下的KDL问题就完美解决了。
既然上面说了是在单线程里,言外之意就会存在多线程问题,例如:
Singleton& Singleton::Instance( void )
{
if ( !ms_pInstance )
{
ms_pInstance = new Singleton;
}
return *ms_pInstance;
}
假如有两个线程要访问这个Instance,第一个线程进入Instance函数,并检测if条件,由于是第一次进入,if条件成立,进入了if,执行到红色代码。此时,有可能被OS的调度器中断,而将控制权交给另外一个线程。
第二个线程同样来到if条件,发现ms_pInstance还是为NULL,因为第一个线程还没来得及构造它就已经被中断了。此时假设第二个线程完成了new的调用,成功的构造了Singleton,并顺利的返回。
很不幸,第一个线程此刻苏醒了,由于它被中断在红色那句代码,唤醒之后,继续执行,调用new再次构造了Singleton,这样一来,两个线程就构建两个Singleton,这就破坏了唯一性。
我们意识到,这是一个竞态条件问题,在共享的全局资源对竞态条件和多线程环境而言都是不可靠的。怎么避免上面的这种情况呢,有一种简单的做法是:
Singleton& Singleton::Instance( void )
{
_Lock holder( _mutex );
if ( !ms_pInstance )
{
ms_pInstance = new Singleton;
}
return *ms_pInstance;
}
_mutex是一个互斥体,_Lock类专门用于管理互斥体,在_Lock的构造函数中对_mutex加锁,在析构函数中解锁。这样保证同一在锁定之后操作不会被其它线程打断。holder是一个临时的_Lock对象,在Instance函数结束时会调用其析构,自动解锁。这也是著名的RAII机制。
似乎这样做确实能够解决竞态条件的问题,在一些场合也是可以的,但是在需要更高效率的环境下,这样做缺乏效率,比起简单的if ( !ms_pInstance )测试要昂贵很多。因为每次进入Instance函数都加锁解锁一次,即使需要加锁解锁的只有第一次进入时。所以我们想要有这样的解法:
Singleton& Singleton::Instance( void )
{
if ( !ms_pInstance )
{
_Lock holder( _mutex );
ms_pInstance = new Singleton;
}
return *ms_pInstance;
}
这样虽然解决了效率问题,但是竞态条件问题又回来了,打不到我们的要求,因为两个线程都进入了if,再锁定还是会产生两个Singleton对象。于是有一个比较巧妙的用法,即“双检测锁定”Double-Checked Locking模式。直接看效果吧:
Singleton& Singleton::Instance( void )
{
if ( !ms_pInstance )
{
_Lock holder( _mutex );
if ( !ms_pInstance )
ms_pInstance = new Singleton;
}
return *ms_pInstance;
}
非常美妙,这样就解决了效率问题,同时还解决了竞态条件问题。即使两个线程都进入了第一个if,但第二个if只会有一个线程进入,这样当某个线程构造了Singleton对象,其它线程因为中断在_Lock holder( _mutex )这一句。等到唤醒时,ms_pInstance已经被构造了,第二个if测试就会失败,便不会再次创建Singleton了。第一个if显得很粗糙快速,第二个if显得清晰缓慢,第一个if是为了第二次进入Instance函数提高效率不再加锁,第二个if是为了第一次进入Instance避免产生多个Singleton对象,各施其职,简单而看似多余的if测试改进,显得如此美妙。
本文从开头到现在,一次又一次感到完美,又一次一次发现不足。到此,又似乎感到了完美,但完美背后还真容易有阴霾。虽然上面的双检测锁定已经在理论上胜任了这一切,趋近于完美。但是,有经验的程序员,会发现它还是存在一个问题。
对于RISC(精简指令集)机器的编译器,有一种优化策略,这个策略会将编译器产生出来的汇编指令重新排列,使代码能够最佳运用RISC处理器的平行特性(可以同时执行几个动作)。这样做的好处是能够提高运行效率,甚至可以加倍。但是不好之处就是破坏了我们的“完美”设计“双检测锁定”。编译器很可能将第二个if测试的指令排列到_Lock holder( _mutex )的指令之前。这样竞态条件问题又出现了,哎!
碰到这样的问题,就只有翻翻编译器的说明文档了,另外可以在ms_pInstance前加上volatile修饰,因为合理的编译器会为volatile对象产生恰当而明确的代码。
到此,常见的问题都基本解决了,不管是多线程还是单线程,在具体的环境我们再斟酌选择哪一种方式,因此,本文并没有给出一个统一的解决方案。你还可以将上面的机制组装到一起,写成一个SingletonHolder模板类,在此就不实现了。Singleton还能根据具体进行扩展,方法也不止上面这些,我们只有一个目的,让它正确的为我们服务。
在本文开头说Singleton是一个相对好理解的一种设计模式,但从整篇下来,它也并不是那么单纯。由简单到复杂,每一种设计方案都有它的用武之地,例如,我们的程序里根本就不会出现KDL问题,那么就可以简单处理。再者我们有的Singleton不可能在多线程环境里运行,那么我们也没有必要设计多线程这一块,而只需要在考虑问题时意识到就可以了。做到一切尽在掌握之中即可。
好吧!本文就到此结束,重在体会这些细节的机制和挖掘问题然后解决问题的乐趣。在此感谢《Modern C++ Design》,望大家多提意见,感谢!!