Posts tagged Singleton
Singleton#2 关于全局对象,初始化依赖,死引用,线程安全
0此前曾经总结过一篇关于Singleton实现方式的文章,在这里:
http://www.windameister.org/blog/index.php/2009/01/18/singleton-initialization/
本文是对上文中未能涵盖的一些问题的补充。
首先是关于全局对象的,全局对象的某些缺陷正是促使我们使用Singleton的原因之一。看下面这个例子:
// xxx.cpp
GlobalData g_Global1;
GlobalData2 g_Global2;
在一个编译单元(某个CPP)中定义的全局变量,会按照定义的先后次序进行构造。如上定义的情况下,程序初始化阶段,会先构造g_Global1对象,而后构造g_Global2。然而我们曾经提到过,如果GlobalData的构造函数依赖于g_Global2则会产生问题,因为此时g_Global2还未被构造。
另外,在析构的时候,会按照与构造相反的顺序析构,也即先析构g_Global2再析构g_Global1。然而如果GlobalData的析构函数依赖于g_Global2,也会产生问题,因为到g_Global1析构的时候,此时的g_Global2已经析构完毕了,很可能所访问的地址已经是无效的。
当然我们可以按照对象相互之间初始化的依赖关系来确定其定义的顺序,然而这种感觉似乎不那么靠谱。首先,这种方法只在单个编译单元范围内起作用,而对跨编译单元的全局对象则无解(因为link的时候,会将各个cpp编译出来的obj文件链接到一起,而这个顺序并没有任何标准进行规定,各家编译器厂商都有自己的实现方式),如果cppA中有一个全局对象globalA,cppB中有一个globalB,我们不能推断出这两者的初始化顺序孰先孰后。
Imperfect C++的11.1.3中提到了一种控制全局对象初始化顺序的方案:
GlobalData* g_Global1;
GlobalData2* g_Global2;
int main()
{
GlobalData _global1;
GlobalData2 _global2;
g_Global1 = &_global1;
g_Global2 = &_global2;
…
}
通过将全局对象定义于main函数的栈空间中,我们可以获得对初始化顺序的完全控制权。然而这样的做法也有其特有的缺陷和局限性:
1、 全局指针可能被某个客户代码中的全局对象所拥有,并在析构中针对这个指针做些什么,如果是这样的话,由于全局对象析构时,main函数已经退出,并且其中的对象已经析构完毕,我们又一次遇到了针对已死亡对象的操作。
2、 我们可能并不总能拥有自己编写main函数的权利,当我们在利用某个现有框架进行编码的时候,很可能我们自己并不能拥有对入口函数的控制权,那么这种方法就很难奏效了。
以上是对全局对象的简单总结。事实上,全局对象和Singleton同样面临多线程竞争条件的问题,这也是全局对象不被推荐使用的原因之一。
下面再回到我们的正题Singleton:
Scott Meyers曾经描述过一种单件的实现途径,后被人们称为Meyers单件。大致的实现看起来如下:
class Thing
{
public:
Thing& GetInstance()
{
static Thing s_instance;
return s_instance;
}
private:
Thing(Thing const&);
Thing &operator = (Thing const&);
}
通过GetInstance方法,使用lazy-evaluation的方式来按需创建。(该方法在云风的《游戏之旅——我的编程感悟》一书中也有提及)。上述实现存在几个问题:
1、 局部静态对象——在多线程情况下将会导致竞争条件
2、 基于局部静态对象的前提条件,并且GetInstance方法被实现为内联函数,如果该头文件被两个模块共享,在某些编译器上会导致在不同模块中分别创建单件对象实例的情况。(感兴趣的话,可以用VC6做个实验)
3、 你无法控制该对象的生命期,该对象在第一次被使用时创建,与其他静态对象一起随进程关闭机制来销毁。(准确的说是在Main函数结束之后,会执行到一个专门做析构的代码块,这部分代码由编译器生成,作为程序的编写者,我们无从控制其析构顺序)可能另一个静态对象的析构函数,在s_instance析构之后再调用Thing::GetInstance(),这等于是在与一个已死去的人交谈,着被称为死引用(Dead Reference)。
针对死引用(Dead Reference)问题,Alexandrescu在Loki库中展示了一种Singleton技术,通过将单件对象挂钩到语言清理机制上,以确保他们按照一个相对寿命级别被销毁。这种方式,要求程序员对单件的寿命级别进行手工赋值,然而这很难保证不出错。
Imperfect C++(中文版166页)中提出了另一种针对死引用问题的解决方案,简单的说就是引用计数。假设A是一个单件,模块B,C都要依赖于A,A提供两个函数A_init()和A_Uninit(),这两个函数对A被依赖的次数进行计数,只有到被依赖值为0的时候,才释放自身。B和C在初始化以及销毁时分别调用A_init()和A_Uninit()。这样就可以保证任何一个依赖于A的模块都不会在A已经死亡之后再调用A。在这里,该方案确实能解决死引用问题,只要调用者记得在依赖A的模块中调用A_init()/A_Uninit()即可保证这一点。
关于多线程竞争条件:
此前已经多次提到单件对象存在多线程竞争条件的问题。比如Meyers单件中,情况看起来如下:
Thing& GetInstance()
{
static Thing s_instance;
return s_instance;
}
多线程条件下究竟会发生什么呢?
我们将上述代码改写一下,更容易看清楚发生了什么:经过编译器处理之后的上述代码,看起来类似于下面的样子:
Thing& GetInstance()
{
static bool _s_instance_initialized = false;
static byte _s_instance_mem[sizeof(Thing)];
if (_s_instance_initialized)
return *(Thing*)_s_instance_mem;
new(_s_instance_mem) Thing();
_s_instance_initialized = true;
}
为什么会产生类似于上面这样的代码?
首先,s_instance是一个局部静态变量,其内存会被分配于数据段中,这是为什么会有如下变量:static byte _s_instance_mem[sizeof(Thing)];的原因。
其次,局部静态变量在第一次被访问到的时候初始化,因此编译器必须通过某种手段记录该局部静态变量是否已经被初始化了,这是为什么有static bool _s_instance_initialized = false;的原因。
好了,接下来我们假设有两个线程A,B执行GetInstance(),此前GetInstance尚未被执行过,因此s_instance尚未被构造。
首先,A执行到if (_s_instance_initialized),发现对象尚未被初始化,准备jmp到构造对象的代码(A可能执行到从构造对象到赋值_s_instance_initialized之前的任意代码,不影响接下来所要描述的问题)。此时,线程切换,B执行,B再次发现_s_instance_initialized为false,于是也跳到构造对象的代码,调用了new(_s_instance_mem) Thing();,成功构造对象之后,B将_s_instance_initialized赋值为true,然后继续执行。不久,A线程被唤醒,并再次调用new(_s_instance_mem) Thing();构造对象,且将_s_instance_initialized赋值为true。
至此,实际上该s_instance被构造了两次。
如果想避免上述情况,我们可以对该static对象的构造过程加锁。确保如果有线程正在构造该对象的过程中的话,其他线程必须等待。
Imperfect C++中给出的做法如下:
Thing& GetInstance()
{
static int guard; // 该静态变量加载时会被初始化为0
spin_mutex smx(&guard); // 自旋互斥体,使用对整数guard的原子操作
// 保证当前只有一个线程进入接下来的代码段
lock_scope<spin_mutex> lock(smx); // 一个简单的外附类,
// 调用smx的lock及unlock
static Thing s_instance;
return s_instance;
}
遗憾的是,这种做法使得我们在调用GetInstance()时,无可避免的每次都要做线程同步操作,虽然正常情况下,这种竞争不大可能频繁到让人察觉,上述做法可以称得上是解决竞争条件问题的典型做法了。
前段时间,在阅读《程序员的自我修养——链接装载与库》一书中,我又看到另一个有意思的解决方案,名为double-check。一个典型的double-check的Singleton对象的代码如下:
volatile T* pInst = 0;
T* GetInstance()
{
if (pInst != NULL)
return pInst;
lock();
if (pInst == NULL)
pInst = new T();
unlock();
return pInst;
}
这种写法,一旦pInst被构造完成,后续的访问就不再需要同步操作了。
该书中提出了一个问题,为什么要有第二个对pInst的判断。因为按道理来说,如果此前已经判断过pInst!=NULL,那么后续只管构造对象就好,为什么在lock()之后,需要再一次判断呢?问题就出在“在lock()之后”,因为lock()是同步操作,如果线程B判断pInst != NULL的时候,另一个线程A已经lock()了,并且正在构造对象,但是尚未赋值给pInst,这时候线程B对pInst的判断会导致它继续走到lock(),并且由于线程A已经lock了,此时,线程B进入等待。直到线程A完成对象构造并将pInst赋好值,并且unlock。
如果线程B在lock之后不对pInst进行判断的话,同样可能会出现构造两次的情况。
不过书中提到了基于double-check的代码的另一个问题,该问题实际上源于CPU的乱序执行优化能力。
C++中的new包含了两个过程,分配内存,在分配好的内存上调用构造函数。所以pInst = new T()包含三个步骤:
1. 分配内存
2. 调用构造函数
3. 将内存地址赋值给pInst
问题出在第2,3步上,因为2,3的顺序是可以颠倒的,因此完全可能出现,内存地址先被赋值给了pInst,而对象尚未完全构造完成的状态。此时如果另一个线程获取了CPU,并调用了GetInstance,会发现pInst已经不为0,于是返回了一个尚未完全构造的对象给用户使用。那么这个时候程序会发生什么就取决于该类如何设计了。
该书中还提到,可以使用barrier指令,禁止CPU的乱序执行功能对该逻辑的破坏。具体可以参考《程序员的自我修养》一书。
话说回来,double-check的办法有没有可能被应用在局部静态对象上,从而将同步操作的开销降至最低?答案是显然的,方法如下:
Thing& GetInstance()
{
static Thing* s_pointer = NULL;
if (s_pointer)
return *s_pointer;
lock();
if (!s_pointer)
{
static Thing s_instance;
s_pointer = &s_instance;
}
unlock();
return *s_pointer;
}
不过CPU乱序执行的能力依然可能打乱这个逻辑:可能s_pointer已经被赋值了&s_instance,而此时s_instance的构造函数尚未返回。情况与此前的new T()版本的double-check的Singleton所描述的一致。
自从上次将Singleton常见的形式进行一次总结之后,一直想对Singleton的初始化,竞争条件,以及死引用的问题进行一番总结,本文拖拖拉拉写了几个月,终于在连续一整天没有网络的情况下得以完成,非常安慰~
最后一个收获是,好书应该反复读,每次重读都可能发现此前被忽略的知识,都可能有新的领悟,对原先理解不深刻的知识,有可能会加深理解。
Singleton#1——关于单件对象初始化的探讨
0凡是使用C++进行开发的人,大都或是了解,或是直接使用过Singleton模式,但是Singleton的多种实现方式有什么差异?不同的实现细节背后究竟蕴含着什么意义?本文试图列举常见的几种不同的Singleton实现方式,考察这些不同的实现方式中的细节差异,并剖析其好处与缺点,试图对Singleton的实现方式做一个小结。
我们在使用C++编写实际项目的时候,往往会有对全局对象/变量的访问需求。对全局变量的不加封装的访问,会导致许多麻烦的问题:譬如在调试中,由于对全局变量的修改被分散在程序的各个角落里,使得我们无法准确判断一个错误是在何时发生的;再比如,在多线程环境下,如果多个线程都需要访问一个变量,则需要做线程同步操作,在没有良好的封装的情况下,这种状况可能会导致完全莫名的coredump。不过本文并不是想说Singleton有什么好处或者有什么作用,仅仅只是想总结Singleton的不同实现方式之间的异同,以及适用场合。
我第一次接触到Singleton模式,是在《Game programming all in one》一书中,该书的示例源代码中采用了以下这种Singleton的实现方式:
class A
{
public:
A()
{
ASSERT(!ms_Singleton);
ms_Singleton = this;
}
static A* GetSingleton() { return ms_Singleton; }
protected:
static A* ms_Singleton;
};
A* A::ms_Singleton = 0;
代码段-1
这种Singleton在使用的时候,需要由使用者进行初始化。并且,如果A类在程序中尚未被实例化之前,其他代码在调用A::GetSingleton的时候,会得到一个空指针。此外,在必要的时候我们可能还需要对类A实例进行显式的释放。
针对Singleton初始化时机的问题,有一种RAII(Resource Acquire Is Initialization)的技术可以保障无论何时,只要我们调用A::GetSingleton,就必然会获取一个有效的A实例指针。
对上述代码段-1我们只需要做少许改动,即可构建一个RAII的Singleton:
class A {
public:
static A* GetSingleton()
{
if (!ms_Singleton)
ms_Singleton = new A;
return ms_Singleton;
}
protected:
A() {}
static A* ms_Singleton;
};
A* A::ms_Singleton = 0;
代码段-2
上述做法相比代码段-1中所展示的做法有一个好处:无论何时我们调用A::GetSingleton,我们都能获得一个有效的指针。而且类A的实例化被类A自身管理,外界不再需要显式构造一个A的实例,与此同时,我们将A的构造函数的访问限制符设置为protected,以防止外界对A的显式构造。
但是,代码段-2中这种做法也存在一个问题,当外界代码不再需要显式构造A的实例时,显式的delete A::ms_Singleton指针的做法非常难堪。虽然我们可以把释放操作放在A的另一个static方法中,但这实际上又是要求我们在程序中的某个地方去显式的调用这一释放函数,与显式的释放内存无异。
上述问题的实质是内存管理问题,解决问题的思路其实也可以从内存管理的角度出发。我们知道程序中的内存分配方式有以下几种:数据段,堆,栈。
代码段-2中的RAII的实现是将A实例化在堆内存中。栈因为其自身特性(仅适合于局部变量的特性),并不适用于Singleton对象的存放需求,那么数据段是否可行呢?
下面一种实现,使用类作用域静态对象,将A的实例存储于数据段上,因为没有将数据置放于堆内存中,因此可以回避上述实现中遇到的内存释放的问题。
class A
{
public:
A() {}
static A* GetSingleton() { return &s_Instance; }
protected:
static A s_Instance;
};
A A::s_Instance;
代码段-3
这种做法是将单件实例用一个全局静态对象保存。全局静态对象会在程序入口点之前被构造。这带来一个好处:我们可以确保程序入口点之后的任何代码访问到该单件对象时,它都已经被构造完毕了。但这种实现方式同时这也带来一个问题:两个全局静态对象的初始化如果存在依赖关系,我们无从确定哪一个会先被构造。
用一个例子来说明,如果我们无法确信两个静态对象中的哪一个会被先初始化,那么我们可能会遇到什么问题:
class MySingleton1
{
public:
MySingleton1() : m_bIsInited(true) { }
static MySingleton1* getSingleton() { return &s_Instance; }
bool IsInited() { return m_bIsInited; }
private:
static MySingleton1 s_Instance;
bool m_bIsInited;
};
MySingleton1 MySingleton1::s_Instance;
class MySingleton2
{
public:
MySingleton2() { m_strBasePath = MySingleton1::getSingleton()->IsInited() ? “C:\” : “”; }
static MySingleton2* getSingleton() { return &s_Instance; }
private:
static MySingleton2 s_Instance;
std::string m_strBasePath;
};
MySingleton2 MySingleton2::s_Instance;
代码段-4
上述代码是一个简单的例子,MySingleton2依赖MySingleton1中的一个状态。以此决定自身初始化的结果。这个例子是虚构的,但是在实际项目中,如果我们采用类似的方法来初始化配置文件以及相关对象的话,那么也同样会遇到类似的问题。MySingleton2必须要在MySingleton1之后被构造,我们如何保障这一点呢?
显然类作用域或全局作用域的静态对象无法为我们提供这种保障。
既然类作用域的静态对象无法为多个Singleton对象相互依赖的情况提供支持,那么函数作用域的方法如何呢?
下面这种Singleton的实现方法,我是从云风老大的《游戏之旅——我的编程感悟》中看到的。对代码段-3的做法也只需要做小小的改动即可:
class A
{
protected:
A() {}
public:
static A* GetSingleton();
};
A* A::GetSingleton()
{
static A _instance;
return &_instance;
}
代码段-5
在GetSingleton()函数中,我们定义了一个函数作用域的静态实例A _instance。由于静态数据会由编译器自动为其在数据段中留存空间,因而我们同样不需要显式的为其分配内存(这里的“显式”是指调用new operator将A实例构造于堆空间中)。而且,前面的依赖问题,在这里不复存在了。由于静态对象被定义在函数作用域当中,因此,该对象会在GetSingleton被第一次访问时构造出来,因此类似于代码段-4中的情况,在这里就不再成为问题了。当MySingleton2对象被构造时,会显式调用MySingleton1::getSingleton函数。此时,如果MySingleton1已经被构造,则直接返回地址,若没有被构造,则会先调用MySingleton1的构造函数,再将其对象地址返回。无论如何,MySingleton2对MySingleton1的依赖都会被正确处理。
为了证明这一点,可以看一下下面这个程序的运行结果:
运行环境是WinXPSP2,编译环境是VC2005。
#include <iostream>
#define CTOR_MSG(className)
cout<<#className<<” Constructed”<<endl
#define DTOR_MSG(className)
cout<<#className<<” Destructed”<<endl
using namespace std;
class Singleton_Impl1 {
protected:
Singleton_Impl1()
: m_bSomeConf(true)
{ CTOR_MSG(Singleton_Impl1); }
public:
~Singleton_Impl1()
{ DTOR_MSG(Singleton_Impl1); }
bool GetSomeConfiguration() { return m_bSomeConf; }
static Singleton_Impl1* getInstance();
private:
bool m_bSomeConf;
};
Singleton_Impl1* Singleton_Impl1::getInstance()
{
static Singleton_Impl1 _instance;
return &_instance;
}
class Singleton_RelyOnImpl1
{
protected:
Singleton_RelyOnImpl1()
{
CTOR_MSG(Singleton_RelyOnImpl1);
if (Singleton_Impl1::getInstance()->GetSomeConfiguration())
cout<<”Do the initialization as Singleton_Impl1′s configuration is true”<<endl;
else
cout<<”Do the initialization as Singleton_Impl1′s configuration is false”<<endl;
}
public:
~Singleton_RelyOnImpl1()
{ DTOR_MSG(Singleton_RelyOnImpl1); }
static Singleton_RelyOnImpl1* getInstance();
};
Singleton_RelyOnImpl1* Singleton_RelyOnImpl1::getInstance()
{
static Singleton_RelyOnImpl1 _instance;
return &_instance;
}
int _tmain(int argc, _TCHAR* argv[])
{
cout<<”Enter Main”<<endl;
Singleton_RelyOnImpl1::getInstance();
cout<<”Leave Main”<<endl;
system(“pause”);
return 0;
}
运行结果如下:
Enter Main
Singleton_RelyOnImpl1 Constructed
Singleton_Impl1 Constructed
Do the initialization as Singleton_Impl1′s configuration is true
Leave Main
可以看到,函数作用域的静态对象在该函数被第一次调用时初始化,并且初始化依赖被正确的处理了。
上面的实现中,当我们每次使用Singleton的时候,都不可避免的要定义一个静态对象或者静态对象指针,以及一个静态类成员函数用以获取单件对象的引用或指针。有没有办法把这些重复的编码消除掉呢?
我们首先想到的就是继承:C++中的继承可以将一些共有的操作和成员放置于基类当中,派生类继承自基类之后,就自然拥有了基类已有的成员函数和成员变量。像这样:
class SingletonBase
{
public:
SingletonBase* getSingleton();
};
class MySingleton1 : public SingletonBase
{
};
现在MySingleton1就继承了来自SingletonBase中的getSingleton()函数。但是,讨厌的事情发生了,我们在使用MySingleton1::getSingleton()时,得到的是一个SingletonBase*的指针。这是件麻烦事,我们不想在获取了指针之后再写讨厌的转型代码,我们希望getSingleton直接返回一个符合我们想要的类型的指针。(MySingleton1::getSingleton()直接返回一个MySingleton1*)。考虑这个需求,我们想到的是:或许可以重载?然而这个念头转眼就被否决了,因为C++无法根据返回值类型进行重载决议。何况,如果我们需要在MySingleton1中写重载函数,那为什么还需要SingletonBase呢?本来在我们的期望中,SingletonBase应该将这些讨厌的事都做了的,我们需要的只是派生一个,然后拿着就用就好~
幸好我们有模板。C++强大的模板功能,为我们提供了这样的可能(以下实现方案的思想来自于Ogre::Singleton的实现,但在具体细节上做了简化。关于Ogre的其他方面的分析文章可以参看这里)。
如果我们将SingletonBase设计为一个模板类,那么上述的头痛问题就迎刃而解了。
template<typename T>
class SingletonBase
{
public:
SingletonBase() { s_pInstance = static_cast<T*>(this); }
~SingletonBase() { assert(s_pInstance); s_pInstance = 0; }
static T* getSingleton() { return s_pInstance; }
protected:
static T* s_pInstance;
};
class MySingleton : public SingletonBase<MySingleton>
{
};
这样一来,我们的MySingleton就立刻拥有了一个static MySingleton* s_pInstance的声明,以及一个static MySIngleton* getSingleton()的定义。
MySingleton的实现文件中还需要给出s_pInstance指针的定义,如下:
template<> MySingleton* SingletonBase<MySingleton>::s_pInstance = NULL;
对于指针s_pInstance的初始化,我们不必操心,因为它在SingletonBase的构造函数中被赋予this的地址。因此只需要构造完成一个MySingleton对象,在其基类部分构造完成时,s_pInstance就已经指向MySingleton的首地址了。(由于早期的编译器在对象内存布局上的一些不符合C++标准的细节,因此在实做中,Ogre在赋值的部分做了一些预编译处理)。
不过这种做法我们虽然摆脱了每次写重复代码的状况,却陷入了必须显式初始化每一个单件对象的境地。
Ogre中是在root对象里对所有的Manager对象(Ogre引擎中的单件对象)进行初始化的。对这类Singleton对象的初始化也非常简单,只需一行代码如下即可:
MySingleton* pMySingleton = new MySingleton;
这样做的好处在于:我们可以完全显式的控制所有的Singleton对象的初始化顺序,而不是像代码段5中的做法一样,将初始化顺序交付给其他代码对Singleton对象的getSingleton函数的调用顺序。
实际上这种做法的本质与代码段1中的做法极其类似,而好处则是:我们不必再写重复代码了。
Singleton的实现有许多种,可能存在许多别的实现方式是本文中没有提及的,针对不同的实现方式,我们应当在实际使用中,根据实际环境以及项目需求,采取合适的做法。
最后,本文也没有涉及在多线程环境下,对Singleton对象的同步问题的说明。事实上,如果Singleton对象被应用在多线程环境中,并应用于跨线程的资源管理,这方面的问题是必须考虑的。
附加:推荐一个wiki链接:http://en.wikipedia.org/wiki/Singleton_pattern
这里对Singleton在各个不同语言环境中的使用做了一个总体概览。其后的Reference等延伸阅读也很有价值。