目录
最近项目里边用到了ACE, 里边的很多类都使用了ACE_Singleton(爱立信不少遗留项目,都喜欢用这种比较heavy的组件 :-()。
单件模式的原理虽然简单,易懂,但感觉要用好它,也不是那么容易。下面从几个方面来进行说明。
1. 基本实现
下面代码是单件模式的最基本的实现,仅限于单线程环境。 通过定义一个静态的成员,来保存这个唯一的对象实例。并通过一个静态接口来获取这个唯一的实例。
class Singleton
{
public:
~Singleton()
{
cout<<"Singleton Destructor"<<endl;
}
static Singleton* getInstance()
{
if(NULL==_instance)
{
_instance = new Singleton();
}
return _instance;
}
private:
Singleton()
{
cout<<"Singleton Constructor"<<endl;
};
static Singleton* _instance;
};
Singleton* Singleton::_instance = NULL;
int main()
{
Singleton* sgn1 = Singleton::getInstance();
Singleton* sgn2 = Singleton::getInstance();
if(sgn1 == sgn2)
cout<<"unique instance"<<endl;
system("pause");
return 0;
}
运行结果为:
Singleton constructor
unique instance
Press any key to continue . . .
2. 静态成员的释放
有了new, 就应该有对应的delete. 那到底什么时候释放呢。因为new分配的是堆内存,估计即使加上析构函数,也没有效果。带着这个疑问,做了下实验,给类Singleton加上了一个析构函数。
Singleton::~Singleton()
{
std::cout<<"Singleton destructor"<<std::endl;
}
实验结果显示推测正确,堆内存不会自动释放。
自然的,会想到定义一个释放函数的方法,调用delete Singleton::instance()对静态实例进行释放。但是,这样增加了风险,因为调用者很可能会忘记调了此函数。
调研了些资料,其实有下面的几种方法:
2.1. 使用内嵌类
通过一个内嵌类和一个静态成员来实现自动释放的机制,相当于为单件加了个垃圾回收器。关键点在于static Cleaner clr;这个声明,由于是静态成员,系统会在栈里分配内存,回收工作也就由系统自动完成了。
class Singleton
{
public:
~Singleton()
{
cout<<"Singleton Destructor"<<endl;
}
static Singleton* getInstance()
{
if(NULL==_instance)
{
_instance = new Singleton();
static Cleaner clr;
}
return _instance;
}
private:
Singleton()
{
cout<<"Singleton Constructor"<<endl;
};
static Singleton* _instance;
class Cleaner
{
public:
Cleaner()
{
cout<<"Cleaner Constructor"<<endl;
}
~Cleaner()
{
cout<<"Cleaner Destructor"<<endl;
if(NULL!=Singleton::_instance)
delete Singleton::_instance;
}
};
};
Singleton* Singleton::_instance = NULL;
int main()
{
Singleton* sgn1 = Singleton::getInstance();
Singleton* sgn2 = Singleton::getInstance();
if(sgn1 == sgn2)
cout<<"unique instance"<<endl;
system("pause");
return 0;
}
运行结果为:
Singleton Constructor
Cleaner Constructor
unique instance
Press any key to continue . . .
按任意键退出后,观察到有下面打印:
Cleaner Destructor
Singleton Destructor
2.2. 使用局部静态变量
前面提到的方法,需要新增一个嵌套类,增加了复杂度。
可以对第1节(基本实现)中的getInstance方法进行优化,返回一个局部静态变量。
static Singleton* getInstance()
{
static Singleton instance;
return &instance;
}
程序在结束的时候,系统会自动回收所有的静态/全局内存,这样,单件模式中的静态成员就能被析构掉了。
2.3. 堆内存释放问题
对于第1节(基本实现)中,单例是通过new方法分配的。可能有人会疑问:进程退出时,os会自动回收进程所占用的各种资源,包括栈内存,堆内存,代码指令等等。那为何还会存在第2节所说的资源未释放问题呢?进程退出时,os不就自动释放了么?
其实,有下面这些因素需要考虑:
--现代的大部分os,如windows, linux的确会自动释放,但在一些嵌入式os中,不是自动回收的。
--如果代码是内核级的一些驱动,它造成的内存错误,也是无法自动回收的。除非重启os。
--良好的编程习惯约束,本程序分配的所有资源,都应该由程序自己去析构或者释放。这个c++开发者的最基本素养。
3. 线程安全问题
3.1 问题起源
所谓线程安全,就是说如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
基于第1节中的基本实现,来验证下它是否线程安全:
#include<Windows.h>
#include<process.h>
#include<iostream>
using namespace std;
class Singleton
{
public:
~Singleton()
{
cout<<"Singleton Destructor"<<endl;
}
void DoSomething()
{
cout<<"Singleton DoSomething"<<endl;
}
static Singleton* getInstance()
{
if(NULL == _instance)
_instance = new Singleton();
return _instance;
}
private:
Singleton()
{
static int count = 0;
cout<<"Singleton Constructor:"<<++count<<endl;
};
static Singleton* _instance;
};
unsigned __stdcall thread( void* )
{
Singleton* p = Singleton::getInstance();
p->DoSomething();
return 0;
}
Singleton* Singleton::_instance = NULL;
int main( int argc, void* argv[] )
{
for( int i = 0; i < 10; ++i )
{
HANDLE t = (HANDLE)_beginthreadex( NULL, 0, thread, NULL, 0, NULL );
CloseHandle(t);
}
system("pause");
return 0;
}
运行结果为(注意:下面看似格式有些错乱,其实是程序的原始输出,并不是排版问题):
Singleton Constructor:1
Singleton DoSomething
Singleton Constructor:5Singleton Constructor:3
Singleton DoSomething
Singleton Constructor:4
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton Constructor:6
Singleton DoSomething
Singleton Constructor:2
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Press any key to continue
3.2. 线程安全实现方案
3.2.1 Lazy initialization
也称为慢初始化方法。即第一次调用该类实例的时候才产生一个新的该类实例,并在以后仅返回此实例。
此种方法,需要用锁来保证其线程安全性。因为如上3.1所示,多个线程可能会同时进入到判断实例是否存在的if语句中,它是非线程安全的。
方案一:Double-Check
可以使用double-check来保证线程安全,这也是ACE_Singleton采用的方法。但是如果处理大量数据时,该锁可能会成为严重的性能瓶颈。
实现原理如下:
Singleton* Singleton::getInstance()
{
if(NULL == _instance)
{
Lock();
if(NULL == _instance)
_instance = new Singleton();
Unlock();
}
return _instance;
}
#include<Windows.h>
#include<process.h>
#include<iostream>
using namespace std;
class Singleton
{
public:
~Singleton()
{
cout<<"Singleton Destructor"<<endl;
}
void DoSomething()
{
cout<<"Singleton DoSomething"<<endl;
}
static Singleton* getInstance()
{
if(NULL == _instance)
{
WaitForSingleObject(mtx, INFINITE);
if(NULL == _instance)
_instance = new Singleton();
ReleaseMutex(mtx);
}
return _instance;
}
static HANDLE mtx;
private:
Singleton()
{
static int count = 0;
cout<<"Singleton Constructor:"<<++count<<endl;
};
static Singleton* _instance;
};
unsigned __stdcall thread( void* )
{
Singleton* p = Singleton::getInstance();
p->DoSomething();
return 0;
}
Singleton* Singleton::_instance = NULL;
HANDLE Singleton::mtx=INVALID_HANDLE_VALUE;
int main( int argc, void* argv[] )
{
Singleton::mtx = CreateMutex(NULL, FALSE, (LPCWSTR)"Mutex");
if(NULL==Singleton::mtx)
return 0;
for( int i = 0; i < 10; ++i )
{
HANDLE t = (HANDLE)_beginthreadex( NULL, 0, thread, NULL, 0, NULL );
CloseHandle(t);
}
Sleep(100);
CloseHandle(Singleton::mtx);
system("pause");
return 0;
}
Press any key to continue . . .
方案二:内部静态实例
此方法,就是2.2中所介绍的方法的改进。实现原理如下:
{
Lock();
static Singleton instance;
Unlock();
return &instance;
这里需要注意的是,C++0x以后,要求编译器保证内部静态变量的线程安全性,可以不加锁。但C++0x以前,仍需要加锁。这里是无锁版本:
#include<Windows.h>
#include<process.h>
#include<iostream>
using namespace std;
class Singleton
{
public:
~Singleton()
{
cout<<"Singleton Destructor"<<endl;
}
void DoSomething()
{
cout<<"Singleton DoSomething"<<endl;
}
static Singleton* getInstance()
{
static Singleton instance;
return &instance;
}
private:
Singleton()
{
cout<<"Singleton Constructor"<<endl;
};
static Singleton* _instance;
};
unsigned __stdcall thread( void* )
{
Singleton* p = Singleton::getInstance();
p->DoSomething();
return 0;
}
Singleton* Singleton::_instance = NULL;
int main( int argc, void* argv[] )
{
for( int i = 0; i < 5; ++i )
{
HANDLE t = (HANDLE)_beginthreadex( NULL, 0, thread, NULL, 0, NULL );
CloseHandle(t);
}
Sleep(10);
system("pause");
return 0;
}
运行结果为:
Singleton Constructor
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Press any key to continue . . .
3.2.2 Eager Initialization
也称为急切初始化。即无论是否调用该类的实例,在程序开始时就会产生一个该类的实例,并在以后仅返回此实例。静态初始化实例保证了其线程安全性。因为静态实例初始化在程序开始时进入主函数之前就由主线程以单线程方式完成了初始化,不必担心多线程问题。
在性能需求较高时,可以使用这种模式,避免频繁的锁争夺。实现如下:
#include<Windows.h>
#include<process.h>
#include<iostream>
using namespace std;
class Singleton
{
public:
~Singleton()
{
cout<<"Singleton Destructor"<<endl;
}
void DoSomething()
{
cout<<"Singleton DoSomething"<<endl;
}
static Singleton* getInstance()
{
if(NULL == _instance)
_instance = new Singleton();
return _instance;
}
private:
Singleton()
{
cout<<"Singleton Constructor"<<endl;
};
static Singleton* _instance;
};
unsigned __stdcall thread( void* )
{
Singleton* p = Singleton::getInstance();
p->DoSomething();
return 0;
}
Singleton* Singleton::_instance = new Singleton;
int main( int argc, void* argv[] )
{
for( int i = 0; i < 5; ++i )
{
HANDLE t = (HANDLE)_beginthreadex( NULL, 0, thread, NULL, 0, NULL );
CloseHandle(t);
}
Sleep(10);
system("pause");
return 0;
}
运行结果如下:
Singleton Constructor
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Singleton DoSomething
Press any key to continue . . .
4. 总结
使用单件模式时,需要注意的几个方面
4.1 相互引用
任意两个 Singleton 类的构造函数不能相互引用对方的实例, 否则会导致程序崩溃. 如:
SingletonA& SingletonA::Instance() {
const SingletonB& b = SingletonB::Instance();
static SingletonA theSingleton;
return theSingleton;
}
SingletonB& SingletonB::Instance() {
const SingletonA & b = SingletonA::Instance();
static SingletonB theSingleton;
return theSingleton;
}
4.2 多线程环境下
在多线程的应用场合下必须小心使用。如果唯一实例尚未创建时, 有两个线程同时调用创建方法, 且它们均没有检测到唯一实例的存在, 便会同时各自创建一个实例, 这样就有两个实例被构造出来, 从而违反了单例模式中实例唯一的原则。 解决办法即第3节所讲。
4.3 多实例析构
多个 Singleton 实例相互引用的情况下, 需要谨慎处理析构函数. 如: 初始化顺序为 SingletonA » SingletonB » SingletonC 的三个 Singleton 类, 其中 SingletonA SingletonB 的析构函数调用了 SingletonC 实例的成员函数, 程序退出时, SingletonC 的析构函数 将首先被调用, 导致实例无效, 那么后续 SingletonA SingletonB 的析构都将失败, 导致程序异常退出。