[原] blade中C++ singleton的实现

最近看了龚大大KalyGE中的singleton, 觉得非常不错(C++中线程安全并且高效的singleton).

可惜blade的代码都是C++03的, 没有使用C++11的任何特性. 笔者对于singleton也有些经验, 不过由于业余写代码本来就时间不够(blade在6年内堆了近20W行代码), 所以笔记记得非常少. 最近几个月业余没有写代码了, 所以有时间把blade在开发中遇到的问题贴出来.

而C++11虽然很爽很方便, 但是目前还没有加入支持的打算.

 

为了方便分析, 列出两种方式的singleton实现:

简单实现 (local static object):

1 template<typename T> class Singleton

2 {

3     static T& getSingleton()

4     {

5         static T instance;

6         return instance;

7     }

8 };

复杂实现( double check lock):

 1 static T* msSingleton = NULL;

 2 static Lock msLock;

 3 

 4 T& getSingleton()

 5 {

 6     if (NULL == msSingleton )

 7     {

 8         ScopeLock lock(msLock);

 9         if (NULL == msSingleton )

10         {

11             msSingleton = new T;

12         }

13     }

14      return *msSingleton;

15 }

下面两种实现, 称为简单模式和复杂模式, 并基于C++03标准分析.

 

1.定义和限制

singleton顾名思义, 就是单例, 所以不能有多个实例, 也不能复制. 笔者曾在工作中就遇到过新手类似下面的复制:

1 A a = A::getSingleton();

即便是老鸟, 也难以保证不会有手抖的时候. 这种问题可以在runtime做检查. 不过在编译期排除错误更好: 把ctor, copy  ctor设为私有, 同时把operator new设置为protected.

然而有时候为了更加灵活, 允许singleton的一次性new, 比如数据加载时, 统一用new创建对象, 又比如, Singleton的MFC类, MFC在PostNcDestroy的时候, 会自动delete this, 那么要求对象必须new出来

这些情况,可以考虑允许operator new, 并加上runtime check.同时, 为了防止new情况msSingleton无法初始化, 将它的初始化放在构造函数里:

 

1 Singleton

2 {

3     assert(msSingleton == NULL );

4     msSingleton = static_cast<T*>(this);

5 }

 

 

 

 

2.多线程

多线程的问题, 龚大大已经说的很详细了, 这里不多说了.

因为没有使用C++11, 目前blade的singleton使用的是double check lock, 可惜还没有加memory barrier, 考虑后面可能会加上.

对于多线程来说, 个人还是认为, singleton应该在程序初始化时就构建好, 这样可以简化singleton的实现.

 

3.构造和析构顺序

先贴以下笔者之前的笔记:

http://wenzhang.baidu.com/page/view?key=0f256d18fe5d14a2-1426750048

http://wenzhang.baidu.com/page/view?key=f06f2a30029f88ef-1426750071

而对于#pragma init_seg这种东西暂时忽略, 因为如果可以通过标准定义可以解决, 那么跨平台和移植性就有保证, 所以尽量不考虑compiler pragma.

 

对于静态变量的初始化/销毁顺序, 这里就不引用C++03标准的原文了, 简单描述如下:

  • 单个编译单元内的静态变量, 按顺序初始化
  • 不同编译单元内的静态变量, 初始化顺序是不确定
  • 函数内的局部静态变量, 在第一次执行到变量定义的时候,执行初始化
  • 所有静态变量的析构顺序与构造顺序相反, 逆序执行

 

贴两个blade在开发中遇到的问题:

问题a: 初始化

 1 class A : Singleton<A> {};

 2 class B

 3 {

 4     B()

 5     {

 6         A::getSingleton().xxxx();

 7     }

 8 };

 9 

10 static B b;

以上情况对于简单模式的singleton没有问题, 单例A会在第一次调用时初始化.
但是对于复杂模式呢?

注意Singleton<A>::msSingleon和b都是非局部静态变量. 对于模板来说, 模板实例化的位置不确定, 不知道在哪个编译单元, 所以可以认为::b和::Singleton<A>::msSingleton的初始化顺序是不确定的.

当b先于Singleton<A>::msSingleton初始化的时候, 由于b的构造调用的A的singleton lazy init,  而后, ::Singleton<A>::msSingleton又接着初始化, 根据用户定义

1 static T* msSingleton = NULL;

将Singleton<A>::msSingleton指针清空! 这是lazy init在指针初始化之前, 给指针赋值了, 不用去查标准如何定义该行为, 很明显在逻辑上已经错了.

解决方法是不指定initializer, 这样local static的对象只会被清零(zero-initialization), 而且是最先执行清零, 而后不再被赋值.  (C++03: 6.7 Declaration statement: 4).

1  static T* msSingleton;

题外: C++的NULL被定义为0, 所以清零没有问题. 而且指针可以被隐式转换为bool, 所以C++是鼓励用户使用if( ptr )的, 而C没有明确规定NULL的值, 甚至在某些机器上, NULL 不是0.所以if(ptr)对于C来说就"呵呵"了.而blade的代码全部是C++, 但全使用类似if( ptr == NULL )的格式, 这个只是习惯而已. 当然理论上(NULL == ptr)更好.

 

问题b: 析构

然而再看下面的情况:

1 class A: Singelton<A> {}

2 class B: Singleton<B>

3 {

4     ~B()

5     {

6         A::getSingleton().xxxx();

7     }

8 };

B在析构时依赖了A, 所以必须要求A在B之前构造. 但是由于lazy init的原因, 方便嘛, 可能使用的时候就很随意, 导致没有控制初始化顺序, 那么析构顺序也不对了.

但对于简单模式的Singleton, 至少顺序还是能控制的:

1 B::getSingleton().xxxx();  //lazy init of B

2 ...

3 A::getSingleton().xxxx(); //lazy init of A

把上面的代码改为:

1 A::getSingleton();         //force (explicit) init of A,B: A must construct before B

2 B::getSingleton();

3 ...

4 

5 B::getSingleton().xxxx();

6 ...

7 A::getSingleton().xxxx();

8 ...

这样A在B之前初始化, 所以A能够在B之后析构.

 

而对于复杂模式的Singlton, 因为使用的是指针. 为了完成lazy de-init, 将msSingleton, 改为智能指针, 这样可以自动析构. 同样为了解决问题a, 这个智能指针不能有initializer.

然而, 不同的singleton内的静态变量, 由于其初始化顺序用户无法控制, 所以析构顺序根本无法控制,成了硬伤.

 

分析: 简单模式(local static object)的单例, 至少是可以控制析构顺序的, 而复杂模式(double check lock), 直接成了硬伤. 那么把简单模式应用到复杂模式, 会怎么样?

local static smart pointer:

 1 T* msSingleton;

 2 

 3 T& getSingleton()

 4 {

 5     if (NULL == msSingleton )

 6     {

 7         static Lock msLock;

 8         ScopeLock lock(msLock);

 9         if (NULL == msSingleton )

10         {

11             static SMART_PTR<T> p;

12             p = new T;

13             msSingleton = p;

14         }

15     }

16      return *msSingleton;

17 }

由于local static smart pointer的存在, 就可以控制lazy de-init的顺序了. 这种模式对于问题a和问题b都可以解决顺序问题. 对于问题b, 因为初始化顺序比较随意, 除非事先仔细分析, 否则要等析构出问题了才能修复.

然而lazy init就是为了方便, 使程序员可以不管这些细节, 集中精力做业务逻辑. 所以等出了问题再改也不是不能接受. 至少析构顺序是可控制的, 而不像之前, 那么痛苦的根本不能控制顺序.

 

为了方便检测问题b, 不至于崩溃在深层调用后, 还要费神查看调用栈, 加上一个assert提前拦截到错误就可以了.

 1 T* msSingleton;

 2  

 3 ~Singleton()

 4 {

 5     msSingleton = NULL;

 6 }

 7 

 8 T& getSingleton()

 9 {

10     if (NULL == msSingleton )

11     {

12         static bool isCreated = false;

13         assert( isCreated == false ); //avoid create for the second time, this usually happens when singleton construction order need explicit control

14 

15         static Lock msLock;

16         ScopeLock lock(msLock);

17         isCreated = true;

18         if (NULL == msSingleton )

19         {

20             static SMART_PTR<T> p;

21             p = new T;

22             msSingleton = p;

23          }

24      }

25      return *msSingleton;

26 }

在这种情况下, Singleton析构时msSington会被清空. 如果之后还有调用, 那么就会触发assertion failure.

因为这种情况非常少, blade至今遇到过3-5次, 如果遇到了, 调整一下调用顺序, 或则按上面问题b的处理方式, 提前显式调用一次, 强制初始化就可以了.

个人以为, 这种方式比纯静态变量要方便, 不用仔细分析依赖. 况且, 即便提前分析了依赖, 为了稳健性, 往往还要加上assert确保依赖没有出错, 而singleton将assert写入模板, 避免了重复手写.

 

上面的两个问题是blade的singleton在开发中遇到的典型问题, 理论上可能还有其他问题没有记录, 或者没有遇到也没有解决, 但是俗话说, 车到山前必有路, 总会有个解决方案的, 比如修改设计和实现等等.

只要singleton的初始化顺序是用户(程序猿)可控的, 而不是无序执行, 用户无法控制, 问题都好说.

比如在问题a中, 如果也有析构的依赖: A在B之后析构.那么要求A在B之前构造, 可以多加一个静态变量:

 

class A : Singleton<A> {}

class B

{

    B()

    {

        A::getSingleton().xxxx();

    }

    ~B()

    {

        A::getSingleton().yyyy();

    }

};



template <typename T> class SingletonInitializer

{

    SingletonInitializer()

    {

        T::getSingleton();

    }

};



static SingletonInitializer<A> c;

static B b;

 

 

 

 

 

4. 动态库(.DLL/.so)的导出

如果Singleton没有用模板, 那么这个问题可以忽略. 如果使用了模板, 那么就得考虑动态库导出的问题了, 这个问题可以转化为模板类的导出问题:
[原]DLL导出实例化的模板类

GCC对于MSVC, 区别比较大, 细节先不写了, 主要的原理和思路笔者在这中间提到一些:

[原]跨平台编程注意事项(三): window 到 android 的 移植

具体细节有空了再补上.

 

5.内存模型

singleton 需要考虑内存管理吗? 绝大多数情况都不需要. 然而对于笔者这样一个完美主义的强迫症来说, 一想到lazy init的singleton在程序运行的任何时间, 都有可能申请一块内存(第一次初始化时), 并且永不释放, 就心里别扭.

会不会产生内存碎片? 理论上会. 况且一个singleton object内部还有数据, 这些数据也是同样的问题.

一种方式是在double check lock的内部, 仍然使用local static object, 而不使用local static smart pointer, 这样静态变量的内存不是动态分配(堆内存), 不需要特殊的管理. 这种方式最简单, 也是目前blade使用的方案.

 1 //note: DO NOT specify a initializer for it

 2 //Otherwise some early lazy initialization will be overwritten by initializer.

 3 T* msSingleton;

 4 

 5 Singleton()

 6 {

 7     assert(msSingleton == NULL );

 8     msSingleton = static_cast<T*>(this);

 9 }

10  

11 ~Singleton()

12 {

13     msSingleton = NULL;

14 }

15 

16 T& getSingleton()

17 {

18     if (NULL == msSingleton )

19     {

20         static bool isCreated = false;

21          assert( isCreated = false ); //avoid create for the second time, this usually happens when singleton construction order need explicit control

22 

23         static Lock msLock;

24         ScopeLock lock(msLock);

25         isCreated = true;

26         if (NULL == msSingleton )

27         {

28             static volatile T p;

29             assert( msSingleton == &p );

30          }

31      }

32      return *msSingleton;

33 }

如果是local static smart pointer, 那么需要动态分配, 由于blade已经有static memory pool, 所以这里也用上了, 将singleton 对象占用的内存, 放在static memory pool中. 不过缺点是, 这样让singleton的实现变得又更加复杂了...

 虽然看起来并不是那么复杂:

1 template<typename T> class Singleton : public StaticAllocatable

2 {

3 ...

4 };

 不过前面也提到了, 即便使用local static object, 但是singleton也有可能是new出来的, 如果这种情况允许, 虽然情况不多, 加上内存管理会更好. 这样即便使用smart pointer也没有问题了.

singleton的内存管理思路, 是基于生命周期的内存管理, 笔者在另一个博客也简单提了思路:

[原]基于内存生命周期的内存管理

显然static memory不是为singleton专门设计的, 它的用处也不止于此, 比如游戏的database, 等等各种静态数据, 都可以放入static pool中去. singleton只是享受了一下便利罢了.

还比如对应的还有标准容器扩展, 只要实现了符合std标准的allocator就可以了. 比如staticvector, staticmap static set等等, 这些都可以作为singleton object的成员变量来使用.

不过内存管理是另一个巨话题, 这里不能多说了, 否则又将是另一个长篇大论了.

 

6.抽象单例

Abstract/Interface Singleton

你可能感兴趣的:(Singleton)