1.1 Singleton(单实例)
如果我们觉得一个类只需要产生一个实例,我们就把它设计为Singleton(单实例模式)。
Singleton是在我们项目中使用频率最高的一个模式,该模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。
Singleton是一种经过改进的全局变量,该模式的描述很简单,但实现却很复杂。特别是Singleton对象的生命周期管理是实现Singleton时最伤脑筋的地方。
本文将讨论以下几个主题:
l 与单纯的全局对象相比,Singleton的特性
l 用以支持单实例的C++基本手法
l 资源泄漏问题与Meyers Singleton
l Dead Reference问题
l Phoenix Singleton
l 带寿命的Singleton
l 多线程问题
l 在动态库中使用Singleton
1.1.1 静态数据+静态函数 != Singleton
Singleton好像可以由静态成员变量+静态成员函数来取代?
例如:
Class Font{…};
Class PrintPort{…};
Class PrintJob {…};
Class MyOnlyPrinter
{
Public:
Static void AddPrintJob(PrintJob& newJob)
{
If(printQueue_.empty() && printingPort_.available())
{
printingPort_.send(newJob.Data());
}
Else
{
printQueue_.push(newJob);
}
}
Private:
Static queue<PrintJob> printQueue_;
Static PrinterPort printingPort_;
Static Font defaultFont_;
}
PrintJob somePrintJob(“MyDocument.txt”);
MyOnlyPrinter::AddPrintJob(somePrintJob);
上述代码有哪些缺点?
我认为主要缺点有两点:
n 代码的初始化和清理可能会有麻烦。
n 初始化工作在程序一启动时就已经完成,如果静态函数到程序退出时都没有调用,我们的初始化工作就白做了。
1.1.2 用以支持Singleton的一些C++基本手法
单实例的原始版实现代码:
class Singleton
{
public:
static Singleton *Instance(); // 单例操作
private:
Singleton();//关闭缺省构造函数
Singleton(const Singleton&);//关闭拷贝构造函数
Singleton& operator=(const Singleton&);//关闭赋值运算符
~Singleton();//避免被外界delete
static Singleton *m_Instance; // 单例指针
};
// 创建单例指针并初始化
Singleton *Singleton::m_Instance = NULL;
// 单例操作实现
Singleton *Singleton::Instance()
{
if (NULL == m_Instance)
{
m_Instance = new Singleton();
}
return m_Instance;
}
// 构造函数
Singleton::Singleton()
{
}
// 析构函数
Singleton::~Singleton()
{
}
通过上述代码,Singleton对象的唯一性在编译期就已经实现,这正是C++实现Singleton设计模式的精髓所在。
除了使用New操作符动态分配内存来实现Singleton,我们还经常通过静态变量实例方式来实现这个模式,下面给出了一种用静态变量方式实现Singleton的代码。
class Singleton
{
public:
static Singleton &Instance(); // 单例操作
private:
Singleton();//关闭缺省构造函数
Singleton(const Singleton&);//关闭拷贝构造函数
Singleton& operator=(const Singleton&);//关闭赋值运算符
~Singleton();//避免被外界delete private:
static Singleton m_Instance; // 静态单例变量
};
// 创建单例
Singleton Singleton::m_Instance;
// 单例操作实现
Singleton &Singleton::Instance()
{
return m_Instance;
}
因为是类的静态变量,上面的代码在进程刚开始启动时就实例化了Singleton对象。
这有两个缺点:
n 初始化工作在程序一启动时就已经完成,如果静态函数到程序退出时都没有调用,我们的初始化工作就白做了;
n 初始化顺序无法确定,这可能会遇到麻烦,例如:
#include “Singleton.h”
Int global = Singleton::Instance()->DoSomething();
由于无法保证编译器先初始化m_Instance,再初始化global全局变量,所以有可能在程序启动时就发生异常。
因此,虽然上述实现简单,但建议抵制住这种诱惑。
1.1.3 Meyers Singleton
对于上一节讨论的指针实现方法,我们有一个问题一直没有讨论,就是Singleton对象何时析构的问题:Singleton对象应该在何时摧毁自身实体?GoF著作中并没有讨论这个主题,事实上,这个问题确实很棘手。
其实,就算Singleton未被删除,也不会造成内存泄露。此外,当一个进程终止时,所有现代操作系统都能够将进程所用到的内存完全释放。
然而,泄露可能还是存在的,而且更隐蔽更有害,那就是资源泄漏。这是因为Singleton的构造函数可以索求广泛的资源:网络连接,OS的内核对象,IPC方法中的各种句柄,数据库连接等。
避免资源泄漏的唯一正确方法是在程序关闭时期删除Singleton对象。
对于静态变量的初始化方法,有一个改进的版本,通过利用了编译器的奇特技巧,可以摧毁Singleton对象。
Singleton &Singleton::Instance()
{
static Singleton m_Instance; // 静态单例变量
return m_Instance;
}
这一手法是由Scott Meyers(Meyers 1996a,条款26)最先提出,所以被称为Meryars singleton。函数内声明的静态变量函数内的static对象,在函数第一次被调用时初始化。
编译器会产生一些代码,保证在进程退出前析构函数会被调用。实际上在编译器产生的代码中会调用atexit函数,把单实例的析构函数注册到atexit中,保证在进程退出前会调用这个析构函数。
Atexit函数内部维护了一个堆栈,先压入栈的函数指针会后执行。
我们自己也可以显示的在代码中调用atexit,以下是实现代码。
class Singleton
{
…
private:
static void FreeInstance(void); // 释放单例变量函数
};
// 单例操作实现
Singleton *Singleton::Instance()
{
if (NULL == m_Instance)
{
m_Instance = new Singleton();
// 用系统函数atexit告诉系统在程序退出的时候调用FreeInstance
atexit(FreeInstance);
}
return m_Instance;
}
// 释放单例变量函数
void Singleton::FreeInstance(void)
{
if (NULL != m_Instance)
{
delete m_Instance;
m_Instance = NULL;
}
}
1.1.4 Dead Reference问题
Meryers singleton是一种很好的Singleton的实现方式,大多数情况下,我们用这种方法实现Singleton就足够了,但有些情况下还是会有问题。
举个例子,假设有个程序使用了三个Singletons:Keyboard,Display和Log。我们以Meryers singleton来实现这三个Singletons。假设Keyboard和Display先于Log实例化,那么按照atexit后进先出的原则,在进程退出时Log应该先被析构,假设Log析构完成后,Keyboard析构函数调用时发生错误,这时需要调用Log来记日志,但Log已经被析构了,所以程序会崩溃,这就是Dead Reference问题。
首先我们看一个简单的解决方案:
// Singleton.h
class Singleton
{
public:
Singleton& Instance()
{
if (!pInstance_)
{
// Check for dead reference
if (destroyed_)
{
OnDeadReference();
}
else
{
// First call—initialize
Create();
}
}
return pInstance_;
}
private:
// Create a new Singleton and store a
// pointer to it in pInstance_
static void Create();
{
// Task: initialize pInstance_
static Singleton theInstance;
pInstance_ = &theInstance;
}
// Gets called if dead reference detected
static void OnDeadReference()
{
throw std::runtime_error("Dead Reference Detected");
}
virtual ~Singleton()
{
pInstance_ = 0;
destroyed_ = true;
}
// Data
Singleton pInstance_;
bool destroyed_;
... disabled 'tors/operator= ...
};
// Singleton.cpp
Singleton* Singleton::pInstance_ = 0;
bool Singleton::destroyed_ = false;
当出现Dead Reference问题时,程序会直接抛出C++异常,这个方案廉价,简单并且不损失效率。
1.1.5 Phoenix Singleton
上一节的方案虽然消除了不确定行为,但这个解决方案不能令人满意。
《现代C++设计》首先试图通过Phoenix Singleton来解决这个问题,所谓Phoenix,就是传说中可以不断从它自己的灰烬中重生的凤凰鸟。如果能够把Log设计成这样类型的Singleton,就可以解决问题。
class Singleton
{
... as before ...
void KillPhoenixSingleton(); // Added
};
void Singleton::OnDeadReference()
{
// Obtain the shell of the destroyed singleton
Create();
// Now pInstance_ points to the "ashes" of the singleton
// - the raw memory that the singleton was seated in.
// Create a new singleton at that address
new(pInstance_) Singleton;
// Queue this new object's destruction
atexit(KillPhoenixSingleton);
// Reset destroyed_ because we're back in business
destroyed_ = false;
}
void Singleton::KillPhoenixSingleton()
{
// Make all ashes again
// - call the destructor by hand.
// It will set pInstance_ to zero and destroyed_ to true
pInstance_->~Singleton();
}
看起来代码没有问题,但实际上并不成功,不成功的原因是C++标准的一个漏洞引起的。这个漏洞的意思是,当程序在atexit的栈中调用登记的函数时,这个函数本身不能调用atexit函数,否则会产生不可预料的后果(轻则资源泄漏,重则程序崩溃)。
1.1.6 实现“带寿命的Singletons”
对于Dead Reference问题,《现代C++设计》给出了第二种解决方案,使用带寿命的Singletons。
如果可以在程序中设置Singleton的寿命,保证Log的寿命长于Display和Keyboard的寿命就可以了。类似于下面的代码:
// This is a Singleton class
class SomeSingleton { ... };
// This is a regular class
class SomeClass { ... };
SomeClass* pGlobalObject(new SomeClass);
int main()
{
SetLongevity(&SomeSingleton().Instance(), 5);
// Ensure pGlobalObject will be deleted
// after SomeSingleton's instance
SetLongevity(pGlobalObject, 6);
...
}
带寿命的Singleton实现有些复杂,实现代码如下:
namespace Private
{
class LifetimeTracker
{
public:
LifetimeTracker(unsigned int x) : longevity_(x) {}
virtual ~LifetimeTracker() = 0;
friend inline bool Compare(
unsigned int longevity,
const LifetimeTracker* p)
{ return p->longevity_ > longevity; }
private:
unsigned int longevity_;
};
// Definition required
inline LifetimeTracker::~LifetimeTracker() {}
}
namespace Private
{
typedef LifetimeTracker** TrackerArray;
extern TrackerArray pTrackerArray;
extern unsigned int elements;
}
//Helper destroyer function
template <typename T>
struct Deleter
{
static void Delete(T* pObj)
{ delete pObj; }
};
// Concrete lifetime tracker for objects of type T
template <typename T, typename Destroyer>
class ConcreteLifetimeTracker : public LifetimeTracker
{
public:
ConcreteLifetimeTracker(T* p,unsigned int longevity,Destroyer d)
:LifetimeTracker(longevity),
,pTracked_(p)
,destroyer_(d)
{}
~ConcreteLifetimeTracker()
{
destroyer_(pTracked_);
}
private:
T* pTracked_;
Destroyer destroyer_;
};
void AtExitFn(); // Declaration needed below
}
template <typename T, typename Destroyer>
void SetLongevity(T* pDynObject, unsigned int longevity,
Destroyer d = Private::Deleter<T>::Delete)
{
TrackerArray pNewArray = static_cast<TrackerArray>(
std::realloc(pTrackerArray, sizeof(T) * (elements + 1)));
if (!pNewArray) throw std::bad_alloc();
pTrackerArray = pNewArray;
LifetimeTracker* p = new ConcreteLifetimeTracker<T, Destroyer>(
pDynObject, longevity, d);
TrackerArray pos = std::upper_bound(
pTrackerArray, pTrackerArray + elements, longevity, Compare);
std::copy_backward(pos, pTrackerArray + elements,
pTrackerArray + elements + 1);
*pos = p;
++elements;
std::atexit(AtExitFn);
}
static void AtExitFn()
{
assert(elements > 0 && pTrackerArray != 0);
// Pick the element at the top of the stack
LifetimeTracker* pTop = pTrackerArray[elements - 1];
// Remove that object off the stack
// Don't check errors-realloc with less memory
// can't fail
pTrackerArray = static_cast<TrackerArray>(std::realloc(
pTrackerArray, sizeof(T) * --elements));
// Destroy the element
delete pTop;
}
1.1.7 多线程问题
前面的实现代码还没有考虑多线程问题,实际绝大部分情况,程序都是运行在多线程环境下的。
对Singleton加锁的代码可以像以下这样的简单方式:
Singleton& Singleton::Instance()
{
// mutex_ is a mutex object
// Lock manages the mutex
Lock guard(mutex_);
if (!pInstance_)
{
pInstance_ = new Singleton;
}
return *pInstance_;
}
这个好用,但效率不佳。
使用所谓的双检测锁定技术可以解决效率问题:
Singleton& Singleton::Instance()
{
if (!pInstance_) // 1
{
Guard myGuard(lock_); // 2
if (!pInstance_) // 3
{
pInstance_ = new Singleton; // 4
}
}
return *pInstance_;
}
即使使用双检测锁定技术,在实践中代码也不总是正确。有些时候编译器会对上述代码进行优化,从而改变代码执行顺序,导致锁定失效。
为了避免编译器进行不必要的优化,需要在pInstance_声明前添加volatile修饰词。
1.1.8 在动态库中使用Singleton
在动态库中使用Singleton,与动态库中的静态变量的生命周期相关,经过实践,可以得出结论,我们在前面讨论的Singleton技术在动态库下运行是没有问题的。
在动态库被卸载的时候,atexit的栈中保存的指针会被调用,可以保证析构函数被执行。