C++ & Design
从C++到C#——穿行于Native与.Net 托管代码之间
0微软在.Net平台中提供了大量丰富的语言,库,并为之提供了非常强大的开发工具。借用.Net平台,我们可以非常容易的开发基于Windows的应用,与采用C++相比,使用C#语言开发相同功能的软件可以极大的提升开发效率,缩短开发周期,减少Bug。
然而,往往现存有大量的积累代码是基于原生语言开发的,当我们决定采用C#时,必须考虑到与现存的软件组件交互的能力,具体而言——如何在一个以C++语言为主的项目中引入.NET组件?如何在.NET组件中使用C++代码库提供的功能?
在游戏项目中,比较理想的情况是底层的3D引擎采用C++编写,而上层编辑器则采用C#编写,这样可以兼顾引擎的运行效率和编辑器的开发效率。一个值得一提的案例是UDK(Unreal引擎的开发包),其中就采用了C++语言与.NET组件相结合的模式,其界面上相当一部分新功能是采用.NET开发的。
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的初始化,竞争条件,以及死引用的问题进行一番总结,本文拖拖拉拉写了几个月,终于在连续一整天没有网络的情况下得以完成,非常安慰~
最后一个收获是,好书应该反复读,每次重读都可能发现此前被忽略的知识,都可能有新的领悟,对原先理解不深刻的知识,有可能会加深理解。
Ogre源码剖析3–可扩展性&插件机制
0Ogre是一个跨操作系统平台的开源3D引擎,既支持DirectX,也支持使用OpenGL,支持可替换的场景管理算法(BSP, OCT)。为Ogre提供这些灵活可扩展性的基础之一就是其面向插件的设计。
很多常用的软件大都提供了插件接口,用以扩展应用程序设计者最初未想到的功能,比较常见的譬如PhotoShop的滤镜,After Effect中的各种插件(最有名的比如shine),3dMax的插件譬如渲染器,魔兽世界的辅助插件等等。
通常,插件本身通常也需要实现主应用程序所需要的必要接口,从而使得插件可以被应用程序加载执行。此外,插件的实现也需要由主应用程序提供一些接口api,通过这些接口,插件可以对主应用程序的功能进行调用。
插件可以是动态链接库(win32平台上为DLL文件),也可以是以脚本的形式提供的,比如魔兽世界中的插件就是使用lua编写的,插件也可能是某种应用程序自定义的文件,只要该应用程序提供了创建该类文件的方法并实现解析、执行功能即可。(不同的实现形式各有利弊,具体需要参考插件及应用程序所处的运行环境进行取舍)
采用插件的一个巨大的好处,以及很多应用程序中使用插件的主要目的就是,我们可以在不需要改动应用程序本身的情况下扩展应用程序的功能。
在Ogre中,插件被用来提供渲染子系统(RenderSystem),不同类别的图形API被封装在不同的渲染子系统的视线当中,Ogre默认提供了DX和OpenGL的实现,甚至,如果我们乐意,甚至可以只用绘点函数实现一套纯软件的渲染子系统提供给Ogre使用。以此为例,用简单的形式来展示这种实现大概类似于下面这样:
class RenderSystem
{
// … operations that a render system need to support
};
// In DX_RenderSystem.dll (in plugin dx rendersystem )
class DX_RenderSystem : public RenderSystem
{
// … implementation & override of the operations from RenderSystem using DirectX
};
// In GL_RenderSystem.dll (in plugin openGL rendersystem)
class GL_RenderSystem : public RenderSystem
{
// … implementation & override of the operations from RenderSystem using OpenGL
};
// …. We could implement any other rendersystem as we like
在引擎的内部(OgreMain),插件在初始化的时候,将一个渲染子系统的实例创建出来(可能是DX_RenderSystem,也可能是GL_RenderSystem,也可能是其他),并将之挂接到OgreMain的Root对象当中。此后,OgreMain中的Root所有的渲染操作也就可以通过该接口访问插件中创建的渲染子系统了。
本文接下来分析Ogre是如何实现插件的,以及插件是如何与OgreMain主引擎进行配合的。
事实上,想编写一个插件是很简单的,我们只需要约定几个接口,插件将这些接口实现了,主应用程序通过某种机制(配置文件/遍历某个目录)加载插件,并查找接口在插件中是否被实现了,如果实现了,则调用之即可。
一个简单的例子如下:
// in xx.dll
__declspec(dllexport) extern “C” // __declspec(dllexport)告知编译器需要将该函数导出
// extern “C” 告知编译器不要对函数做C++名字重整
const char* GetPluginName(void)
{
return “Test Plugin”;
}
// in application
HMODULE hInst = LoadLibrary(“xx.dll”);
if (!hInst) return;
typedef const char* (*GetPluginNameFunc)(void);
GetPluginNameFunc pFunc = GetProcAddress(hInst, “GetPluginName”);
if (!pFunc) return;
const char* szPluginName = pFunc();
// … do other things
事实上,在C语言里,我们就可以通过这种方式,约定一套需要实现的接口,交给插件实现即可。在这里,插件的功能都是在dll中实现的,我们需要加载dll,保存句柄,并在插件卸载的时候释放相应的资源。在C++中,我们显然可以做的更好一点,比如将dll模块相关的功能以及资源封装起来:
class DynLib
{
public:
bool Load(const char* szPluginPath);
void* GetProcAddress(const char* szProcName);
private:
HMODULE m_hInst;
};
bool DynLib::Load(const char* szPluginPath)
{
m_hInst = LoadLibrary(szPluginPath);
return m_hInst != NULL;
}
void* DynLib::GetProcAddress(const char* szProcName)
{
return ::GetProcAddress(m_hInst, szProcName);
}
这样一来,我们就有了一个DLL的简单封装。
但是一个dll里未必只能有一个插件,假若我们简单的把插件接口设计为一套C导出函数,我们就不得不面对这种局限。经典的做法依旧是抽象,Ogre中定义了一个插件接口,每一个实现了插件接口的实例都代表了一个插件,如下:
class Plugin
{
public:
virtual const String& getName() = 0;
virtual void install() = 0;
virtual void initialize() = 0;
virtual void shutdown() = 0;
virtual void uninstall() = 0;
};
Ogre的每一个插件都需要实现以上接口。该接口提供的功能包括:
install 插件被加载时调用。
initialize 插件被初始化时调用。
shutdown 插件被反初始化时调用。
uninstall 插件被卸载时调用。
不过,单有这个接口,插件还是无法工作。插件中还需要提供两个约定的C导出函数。这个稍后再说。
Ogre在Root对象中统一管理插件,因此Root对象提供了以下与插件相关的接口:
class Root
{
// …
void loadPlugin(const String& pluginName);
void unloadPlugin(const String& pluginName);
void installPlugin(Plugin* plugin);
void uninstallPlugin(Plugin* plugin);
// 以及对应的处理多个插件的接口
// …
};
当loadPlugin函数被调用时,Ogre将加载该插件的dll,从中查找并调用名为dllStartPlugin的导出函数。
对应的,当unloadPlugin函数被调用时,Ogre将从该dll中调用dllStopPlugin函数。并将该dll卸载。
每个插件在实现的时候,都必须提供上述的两个C导出函数。其中,dllStartPlugin负责创建插件实例,并调用Root::installPlugin,将创建好的插件指针传递给Root对象。在这里,dllStartPlugin实际可以创建多个插件实例,并依次调用Root::installPlugin,这样我们就可以在一个DLL中包含多个插件了。在dllStopPlugin时,则需要调用Root::uninstallPlugin,并将插件DLL中创建的plugin实例释放掉。
一个典型的dllStartPlugin的实现像这样:
class D3D9Plugin : public Plugin
{
//…
};
D3D9Plugin* plugin;
__declspec(dllexport) extern “C”
void dllStrartPlugin(void)
{
plugin = new D3D9Plugin();
Root::getSingleton().installPlugin(plugin);
}
__declspec(dllexport) extern “C”
void dllStopPlugin(void)
{
Root::getSingleton().uninstallPlugin(plugin);
delete plugin;
}
Root::getSingleton().installPlugin(plugin)会负责调用插件的install以及initialise操作,并将插件的指针存放起来,以备卸载时使用。
Plugin::install以及Plugin::initialise则分别负责创建OgreMain提供扩展功能接口的实例,以及将创建好的对象挂载到应用程序当中。
一个典型的Plugin的行为像这样:
void BspSceneManagerPlugin::install()
{
mBspFactory = new BspSceneManagerFactory();
}
void BspSceneManagerPlugin::initialise()
{
Root::getSingleton().addSceneManagerFactory(mBspFactory);
}
其中,BspSceneManagerFactory是继承自OgreMain中的SceneManagerFactory的派生类。
( class BspSceneManagerFactory : public SceneManagerFactory {…} )
通过将场景管理器工厂添加到Root对象当中,插件动态的将其功能添加到了OgreMain当中。
PS: SceneManagerFactory是一个用于创建SceneManager的工厂。SceneManager则是被用于场景管理的管理器。具体的SceneManager也根据算法不同而不同,Ogre自带提供了两种类型的场景管理算法插件:Bsp以及Octree(实现了两个对应的插件,分别是Plugin_BSPSceneManager以及Plugin_OctreeSceneManager)。
通过上述方式,OgreMain核心引擎并不需要关心场景管理算法的具体实现,不需要关心渲染子系统的具体实现,不需要关心粒子系统的具体实现,等等。一切这些扩展性的功能都通过插件实现,并在加载时动态挂载到OgreMain当中,供OgreMain的引擎核心使用。
使用插件时几个需要注意的地方:
1, 调用插件DLL方法创建的对象需要交由插件DLL释放。(因为不同的链接单元可能具有不同的内存管理上下文环境,此处的new与彼处的new在语义上未必等同)
2, 调用插件DLL方法获取的插件内对象的引用或指针,在插件DLL卸载之后就是无效的,必须保证不再使用。(比较容易引发问题的一个典型例子是从插件中传递回一个引用计数字符串,当DLL被卸载后,字符串内指向实际数据的指针已经无效,但是在该对象析构时,仍需要访问该指针)
小议Command模式 抽象在实际编程中的应用
0在许多编辑器中,大都实现了Undo / Redo操作。从界面出发,Undo/Redo仅仅只是一种抽象的”提供单一接口行为“的操作,尽管实际的Undo/Redo工作可能会涉及各种不同的具体操作。举个例子,在3dMax这样的软件中,我们可以创建3D实体,可以编辑其自身的各种属性(譬如立方体有长宽高,球有半径),可以移动实体的位置,对其做旋转,缩放操作,乃至对个别的网格顶点,面片的细微调节。其中的每一步,都可以撤销或重做。
我们仔细思考一下,就会明白,撤销和重做实际上是需要保存每一步操作的”被操作实体的两个状态“(分别是该实体在操作发生前和发生后的状态)。
譬如对一个物体做以移动,我们实际上是保存了移动前的位置,和移动后的位置,这样在Undo/Redo时,只需要分别将其置于所需的状态即可。
而不管具体的状态是如何转变的,对于用户来说,撤销/重做都只是一个简单的命令,表现为一个按钮,或者一次Ctrl+Z。这其实就是一种具有同一性表现的动作,在软件实现中,非常适合使用Command模式对其进行抽象。
譬如,我们只需要定义这样的一个基类即可明晰撤销/重做操作的概念接口:
class Command {
public:
virtual ~ Command() = 0 {}
virtual bool Do() = 0;
virtual bool Undo() = 0;
bool Redo() { Do(); }
};
所有的Undo/Redo的状态,都派生自这个Command抽象类,它定义了两个纯虚函数,用于做和撤销。Redo函数只是简单的调用Do()实现其功能。
有了上面这个接口,当我们想添加更多类型操作的Undo/Redo支持的时候,只需要这样做:
class MoveBoxCommand{
public:
MoveBoxCommand(VECTOR3 vNewPos, int boxid)
: m_iID(boxid), m_vNewPos(vNewPos) {}
~MoveBoxCommand() {}
bool Do()
{
m_vOldPos = SomeManager->GetObject(m_iID)->GetPos();
return SomeManager->GetObject(m_iID)->SetPos(m_vNewPos);
}
bool Undo()
{
return SomeManager->GetObject(m_iID)->SetPos(m_vOldPos);
}
private:
int m_iID;
VECTOR3 m_vNewPos;
VECTOR3 m_vOldPos;
};
现在我们就可以对任意的对象支持Move操作的撤销/重做了,事实上,旋转,缩放等其他操作大都与此类似,对添加删除之类的操作需要做一点特别的处理(因为我们不能简单的把实际对象释放掉,否则其他引用了该对象的Command就会出现问题,一种良好的做法是设置删除标志)。
除此之外,我们需要将所有的操作产生的Command实例放在一个管理器中统一管理,假设其被命名为CommandManager。则CommandManager需要负责维护当前的Command,需要在用户调用Undo时,判断是否能够Undo,如果能够,则取出下一个Undo的Command,调用其Undo,并将当前Command的指针或者索引更新位置。
如果用户在某个点做出了新的操作,则需要将当前Command之后的所有Command清空。(如果用户没有做新操作,则可以使用当前Command之后的Command来实现Redo操作的,但是新操作会冲掉位于Redo序列中的元素)
如果撤销/重做的实现有了以上理解,就不难理解为什么几乎所有的软件在用户Undo多步,再采取新动作之后,就无法Redo了。
通过这种方式,我们就可以实现一个具有可扩展性的撤销重做机制了,不论再加入何种类型的新操作,我们都可以设法将其纳入到上述的框架当中。
具体的撤销重做实现还牵扯到许多细节,不过那都与主题关系不大,我们再来看一个例子:线程池。
有时我们可能会想编写一个线程池,用于处理各种并发的任务。线程池从概念上说是一个很简单的东西,有一个线程的集合,里面的很多线程都睡着,当有任务来的时候,一个线程跳出来把任务领走并执行,执行结束后,线程继续回到池子里睡觉。如果有多个任务,那么就会有许多个线程都跳出来,直到池子里没有更多的空闲线程或者任务队列被领空为止。
在Win32平台下编写多线程程序,大致会像这样:
void ThreadProc(PVOID param)
{
// do something
}
// somewhere in some func, we create the thread
int handle = __beginthreadex(ThreadProc, valuePassin);
问题来了:
线程处理函数一般只有一个通用的模板,类似于上述的ThreadProc,线程池中的线程在创建的时候,需要传递一个处理函数地址,但是由于线程池中有许多线程,他们是批量被创建出来的,在创建这些线程的时候,我们无法立即获知所需处理的内容。因此,经济适用的方法是使用一个通用的线程处理函数,所有的线程被创建时传递的都是这个公共的处理函数。(而不是对每一个任务创建一个采用不同线程处理函数的新线程–这就不是线程池了,而是普通的多线程处理)
具体一点,一段提供如上功能的代码如下:(这个简陋的实现仅为演示上述思想,真正的实现需要对线程同步处理得更加规范,对错误意外情况做处理,完善其他必要的功能。P.s.这段代码是在word中编写,不能保证可以编译通过)
class ThreadPool;
void ThreadProc(PVOID param)
{
ThreadPool * pThreadPool = reinterpret_cast< ThreadPool*>(param);
ThreadTaskPool* pTaskPool = pThreadPool->GetTaskPool();
while (true) {
Task* pTask = pTaskPool->PopTask();
If (!pTask)
Sleep(100);
else
pTask->Do();
}
}
class Task {
public:
virtual void Do() = 0;
};
class ThreadTaskPool {
public:
ThreadTaskPool() { ::InitializeCriticalSection(&m_cs); }
Task* PopTask();
void PushTask(Task* pTask);
private:
CriticalSection m_cs;
std::queue<Task*> m_TaskQueue;
};
Task* ThreadTaskPool::PopTask()
{
Lock<CriticalSection> _lock_; // lock is a scope wrapper
if (m_TaskQueue.empty())
return NULL;
Task* pTask = m_TaskQueue.front();
m_TaskQueue.pop_front();
return pTask;
}
void ThreadTaskPool::PushTask(Task* pTask)
{
Lock<CriticalSection> _lock_;
m_TaskQueue.push_back(pTask);
// we may do something else here(raise some event to notify the thread to wake up).
}
class ThreadPool {
public:
void Init();
ThreadTaskPool* GetTaskPool() const;
private:
HANDLE m_hThreads[MAX_THREADS];
ThreadTaskPool* m_pTasks;
};
void ThreadPool::Init()
{
m_pTasks = new ThreadTaskPool;
for(int nIdx = 0; nIdx < MAX_THREADS; ++nIdx)
{
m_hThreads[nIdx] = CreateThread(NULL, 0, ThreadProc, this, 0, NULL);
}
}
上述代码实现了一个简单的线程池,Task接口用于定义任务,一切需要交给线程池处理的任务都派生自该接口。而线程池中的所有线程只在其线程处理函数中做一件事:检查任务管理器中是否能取出新的任务来,如果能就干活,不能就调用Sleep交出时间片。(这里有一个可以优化的地方,我们可以在任务管理器中定义一个事件对象,如果线程取不出任务,则等待该事件对象,如果有新任务被PushTask,则该事件对象被激活,并唤醒一个等待中的线程)
上面的两个例子:编辑器中的撤销/重做,以及线程池处理任务队列,都是实际编程当中对Command模式的典型应用。他们的共同点在于:都有一个统一的动作接口,而接口背后的工作可能多种多样。在实际编程当中遇到具有这种特点的问题,便可以采用这种思想进行抽象,有助于我们编写具有良好扩展性的程序。
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等延伸阅读也很有价值。