C++线程安全的单例模式

一、在GCC4.0以后的环境下:ios

#include <iostream>c++

using namespace std;
template <typename T>
class Singleton
{
public:
static T& getInstance() {
//使用局部静态变量的缺陷就是建立和析构时的不肯定性。因为Singleton实例会在Instance()函数被访问时被建立,所以在某处新添加的一处对Singleton的访问将可能致使Singleton的生存期发生变化。若是其依赖于其它组成,如另外一个Singleton,那么对它们的生存期进行管理将成为一个灾难。
//甚至能够说,还不如不用Singleton,而使用明确的实例生存期管理。”
// Lock(); GCC 4.0以上的编译器保证了内部静态变量的线程安全,能够不须要这句话
//为何c++0X以前须要加Lock,这是由局部静态变量的实际实现所决定的。
//为了能知足局部静态变量只被初始化一次的需求
//,不少编译器会经过一个全局的标志位记录该静态变量是否已经被初始化的信息。
//那么,对静态变量进行初始化的伪码就变成下面这个样子:
//bool flag = false;
//if (!flag)
// {
// flag = true;
// staticVar = initStatic();
// }
static T s;
// UnLock()
cout << "new s" << endl;
return s; //若是返回的是指针可能会有被外部调用者delete掉的隐患,因此这里返回引用会更加保险一些。
}
private:
Singleton() {}
~Singleton(){}
Singleton(const Singleton& other) {}
Singleton& operator = (const Singleton& other) {}
};面试

class Eager_Singleton //饿汉模式
{
private:
Eager_Singleton() {
}
~Eager_Singleton(){
}
Eager_Singleton(const Eager_Singleton& other);
Eager_Singleton& operator = (const Eager_Singleton& other);
private:
static Eager_Singleton s; //在程序开始时进入主函数以前就由主线程以单线程方式执行该语句完成了初始化,
public:
static Eager_Singleton& getInstance() {
return s;
}
};安全

//将Singleton做为一个组件供其余类使用
class SingletonInstance : public Singleton<SingletonInstance> {

};
int main() {
Singleton<Singleton>::getInstance();
Eager_Singleton::getInstance();
return 0;
}多线程

二、在GCC4.0以前,不用锁实现函数

template<typename T>
class Singleton : boost::noncopyable
{
public:
static T& instance()
{
pthread_once(&ponce_, &Singleton::init);
return *value_;
}优化

static void init()
{
value_ = new T();
}
private:
static pthread_once_t ponce_;
static T* value_;
};
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;spa

template<typename T>
T* Singleton<T>::value_ = NULL; 操作系统

三、在GCC4.0以前,用锁实现(转自write pattern line by line的一个面试场景)线程

template <typename T>
class Singleton
{
public:
static T& Instance()
{
if (m_pInstance == NULL)
{
Lock lock;
if (m_pInstance == NULL)
{
m_pInstance = new T();
Destroy();
}
return *m_pInstance;
}
return *m_pInstance;
}

protected:
Singleton(void) {}
~Singleton(void) {}

private:
Singleton(const Singleton& rhs) {}
Singleton& operator = (const Singleton& rhs) {}

void Destroy()
{
if (m_pInstance != NULL)
delete m_pInstance;
m_pInstance = NULL;
}

static T* volatile m_pInstance;
};

template <typename T>
T* Singleton<T>::m_pInstance = NULL;

由于new运算符的调用分为分配内存、调用构造函数以及为指针赋值三步,就像下面的构造函数调用:”

1 SingletonInstance pInstance = new SingletonInstance();

  “这行代码会转化为如下形式:”

1 SingletonInstance pHeap = __new(sizeof(SingletonInstance));
2 pHeap->SingletonInstance::SingletonInstance();
3 SingletonInstance pInstance = pHeap;

  “这样转换是由于在C++标准中规定,若是内存分配失败,或者构造函数没有成功执行, new运算符所返回的将是空。通常状况下,编译器不会轻易调整这三步的执行顺序,可是在知足特定条件时,如构造函数不会抛出异常等,编译器可能出于优化的目的将第一步和第三步合并为同一步:”

1 SingletonInstance pInstance = __new(sizeof(SingletonInstance));
2 pInstance->SingletonInstance::SingletonInstance();

  “这样就可能致使其中一个线程在完成了内存分配后就被切换到另外一线程,而另外一线程对Singleton的再次访问将因为pInstance已经赋值而越过if分支,从而返回一个不完整的对象。所以,我在这个实现中为静态成员指针添加了volatile关键字。该关键字的实际意义是由其修饰的变量可能会被意想不到地改变,所以每次对其所修饰的变量进行操做都须要从内存中取得它的实际值。它能够用来阻止编译器对指令顺序的调整。只是因为该关键字所提供的禁止重排代码是假定在单线程环境下的,所以并不能禁止多线程环境下的指令重排。”

  “最后来讲说我对atexit()关键字的使用。在经过new关键字建立类型实例的时候,咱们同时经过atexit()函数注册了释放该实例的函数,从而保证了这些实例可以在程序退出前正确地析构。该函数的特性也能保证后被建立的实例首先被析构。其实,对静态类型实例进行析构的过程与前面所提到的在main()函数执行以前插入静态初始化逻辑相对应。”

 

引用仍是指针

  “既然你在实现中使用了指针,为何仍然在Instance()函数中返回引用呢?”面试官又抛出了新的问题。

  “这是由于Singleton返回的实例的生存期是由Singleton自己所决定的,而不是用户代码。咱们知道,指针和引用在语法上的最大区别就是指针能够为NULL,并能够经过delete运算符删除指针所指的实例,而引用则不能够。由该语法区别引伸出的语义区别之一就是这些实例的生存期意义:经过引用所返回的实例,生存期由非用户代码管理,而经过指针返回的实例,其可能在某个时间点没有被建立,或是能够被删除的。可是这两条Singleton都不知足,所以在这里,我使用指针,而不是引用。”

  “指针和引用除了你提到的这些以外,还有其它的区别吗?”

  “有的。指针和引用的区别主要存在于几个方面。从低层次向高层次上来讲,分为编译器实现上的,语法上的以及语义上的区别。就编译器的实现来讲,声明一个引用并无为引用分配内存,而仅仅是为该变量赋予了一个别名。而声明一个指针则分配了内存。这种实现上的差别就致使了语法上的众多区别:对引用进行更改将致使其本来指向的实例被赋值,而对指针进行更改将致使其指向另外一个实例;引用将永远指向一个类型实例,从而致使其不能为NULL,并因为该限制而致使了众多语法上的区别,如dynamic_cast对引用和指针在没法成功进行转化时的行为不一致。而就语义而言,前面所提到的生存期语义是一个区别,同时一个返回引用的函数经常保证其返回结果有效。通常来讲,语义区别的根源经常是语法上的区别,所以上面的语义区别仅仅是列举了一些例子,而真正语义上的差异经常须要考虑它们的语境。”

  “你在前面说到了你的多线程内部实现使用了指针,而返回类型是引用。在编写过程当中,你是否考虑了实例构造不成功的状况,如new运算符运行失败?”

  “是的。在和其它人进行讨论的过程当中,你们对于这种问题有各自的理解。首先,对一个实例的构造将可能在两处抛出异常:new运算符的执行以及构造函数抛出的异常。对于new运算符,我想说的是几点。对于某些操做系统,例如Windows,其经常使用虚拟地址,所以其运行经常不受物理内存实际大小的限制。而对于构造函数中抛出的异常,咱们有两种策略能够选择:在构造函数内对异常进行处理,以及在构造函数以外对异常进行处理。在构造函数内对异常进行处理能够保证类型实例处于一个有效的状态,但通常不是咱们想要的实例状态。这样一个实例会致使后面对它的使用更为繁琐,例如须要更多的处理逻辑或再次致使程序执行异常。反过来,在构造函数以外对异常进行处理经常是更好的选择,由于软件开发人员能够根据产生异常时所构造的实例的状态将必定范围内的各个变量更改成合法的状态。举例来讲,咱们在一个函数中尝试建立一对相互关联的类型实例,那么在一个实例的构造函数抛出了异常时,咱们不该该在构造函数里对该实例的状态进行维护,由于前一个实例的构造是按照后一个实例会正常建立来进行的。相对来讲,放弃后一个实例,并将前一个实例删除是一个比较好的选择。”

  我在白板上比划了一下,继续说到:“咱们知道,异常有两个很是明显的缺陷:效率,以及对代码的污染。在过小的粒度中使用异常,就会致使异常使用次数的增长,对于效率以及代码的整洁型都是伤害。一样地,对拷贝构造函数等组成经常须要使用相似的原则。”

  “反过来讲,Singleton的使用也能够保持着这种原则。Singleton仅仅是一个包装好的全局实例,对其的建立若是一旦不成功,在较高层次上保持正常状态一样是一个较好的选择。”

 

Anti-Patten

  “既然你提到了Singleton仅仅是一个包装好的全局变量,那你能说说它和全局变量的相同与不一样么?”

  “单件能够说是全局变量的替代品。其拥有全局变量的众多特色:全局可见且贯穿应用程序的整个生命周期。除此以外,单件模式还拥有一些全局变量所不具备的性质:同一类型的对象实例只能有一个,同时适当的实现还拥有延迟初始化(Lazy)的功能,能够避免耗时的全局变量初始化所致使的启动速度不佳等问题。要说明的是,Singleton的最主要目的并非做为一个全局变量使用,而是保证类型实例有且仅有一个。它所具备的全局访问特性仅仅是它的一个反作用。但正是这个反作用使它更相似于包装好的全局变量,从而容许各部分代码对其直接进行操做。软件开发人员须要经过仔细地阅读各部分对其进行操做的代码才能了解其真正的使用方式,而不能经过接口获得组件依赖性等信息。若是Singleton记录了程序的运行状态,那么该状态将是一个全局状态。各个组件对其进行操做的调用时序将变得十分重要,从而使各个组件之间存在着一种隐式的依赖。”

  “从语法上来说,首先Singleton模式实际上将类型功能与类型实例个数限制的代码混合在了一块儿,违反了SRP。其次Singleton模式在Instance()函数中将建立一个肯定的类型,从而禁止了经过多态提供另外一种实现的可能。”

  “可是从系统的角度来说,对Singleton的使用则是没法避免的:假设一个系统拥有成百上千个服务,那么对它们的传递将会成为系统的一个灾难。从微软所提供的众多类库上来看,其经常提供一种方式得到服务的函数,如GetService()等。另一个能够减轻Singleton模式所带来不良影响的方法则是为Singleton模式提供无状态或状态关联很小的实现。”

  “也就是说,Singleton自己并非一个很是差的模式,对其使用的关键在于什么时候使用它并正确的使用它。”

相关文章
相关标签/搜索