最近看了龚大大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的实现.


更新(08/26/2015)补充: 由于上面为了兼容new, 在基类构造函数里面初始化了子类指针, 而指针初始化后子类还未构造, 所以不管线程怎么安全, 都是会有问题的. 这个问题在单线程下看不出来, 但是多线程下就暴露了. 而它本身不是一个多线程问题, 基本无解, 目前还没有想到更好的做法. 最近把支持new/堆分配的特性去掉了, 不再支持基类自动初始化指针, 牺牲一点灵活性换来稳健性. 对于PostNcDestroy的情况, 可以重载以后什么都不做, 跳过delete this. 对于其他三方库接入可能会有一些限制.
对于抽象工厂可以创建Singleton的情况, 理论上可以解决, 强制使用智能指针来声明所有权, 同时智能指针像C++11一样有deleter, 就可以了, 对于singleton指针的deleter,什么都不做. blade的抽象工厂有创建和删除的方法, 可以绑定到智能指针的deleter. 然而blade的智能指针还不支持deleter, 所以目前去掉new的支持以后, 抽象工厂不能创建singleton. 如果以后迁移到C++11, 那么可以使用std::weak_ptr和std::shared_ptr, 并弃用blade自己的智能指针.

另外, 虽然mutex lock比较重量级, 而blade的Lock是spin lock, 其特性特别适合这种场合: 只有第一次构造会花费一点时间, 后面每次调用都是马上返回. 所以由于spin lock的使用, 效率不会有问题, 可以不考虑double check. 如果不使用double check, 那么spin lock直接将整个function scope覆盖, 保证访问的一致性(spin lock隐含barrier), 自然也不需要memory barrier了:

  1. T& getSingleton()
  2. {
  3. #if __cplusplus < 201103L //C++11 doesn't need manual sync
  4. Lock msLock; //spin lock is fast enough, NO NEED for double check, thus no need for memory barrier
  5. ScopedLock guard(msLock);
  6. #endif
  7. static T instance;
  8. return instance;
  9. }

当然也可以使用double check lock+memory barrier, 效率上或许会好一点点?

再加上下面构造/析构顺序的检查, 目前最新的代码整理如下(比下文新):

  1. /********************************************************************
  2. created: 2009/02/07
  3. filename: Singleton.h
  4. author: Crazii
  5.  
  6. purpose: the Singleton pattern: lazy initialization mode
  7. *********************************************************************/
  8. #ifndef __Blade_Singleton_h__
  9. #define __Blade_Singleton_h__
  10. #include "BladeTypes.h" //NULL
  11. #include "Lock.h"
  12. #include "memory/BladeMemory.h"
  13.  
  14. namespace Blade
  15. {
  16.  
  17. template< typename T >
  18. class Singleton : public StaticAllocatable
  19. {
  20. private:
  21. Singleton(const Singleton&);
  22. Singleton& operator=(const Singleton&);
  23. static void* operator new(size_t);
  24. static void* operator new[](size_t);
  25.  
  26. typedef T* pointer;
  27. public:
  28.  
  29. /* @brief public reference access */
  30. static T& getSingleton()
  31. {
  32. //lazy initialization
  33. return *Singleton<T>::operateSingleton();
  34. }
  35.  
  36. /* @brief public pointer access */
  37. static pointer getSingletonPtr()
  38. {
  39. return Singleton<T>::operateSingleton();
  40. }
  41.  
  42. protected:
  43. Singleton()
  44. {
  45. }
  46.  
  47. virtual ~Singleton()
  48. {
  49. Singleton<T>::operateSingleton(true);
  50. }
  51.  
  52. /* @brief call to create instance */
  53. static T* operateSingleton(bool reset = false)
  54. {
  55. static Lock msLock; //spin lock is fast enough, NO NEED for double check, thus no need for memory barrier
  56. ScopedLock lock(msLock);
  57.  
  58. static T* msPtr = NULL;
  59. #if BLADE_DEBUG
  60. static bool isCreated = false;
  61. #endif
  62.  
  63. if( reset )
  64. {
  65. assert( msPtr != NULL );
  66. msPtr = NULL;
  67. return NULL;
  68. }
  69.  
  70. if( msPtr == NULL )
  71. {
  72. #if BLADE_DEBUG
  73. //avoid repeat destroying & creating 2nd time.
  74. //this usually happens when singleton construction order need explicit control
  75. assert(isCreated == false);
  76. #endif
  77. //local static make sure it constructed right on first call
  78. static T msInstance;
  79.  
  80. msPtr = &msInstance;
  81. #if BLADE_DEBUG
  82. isCreated = true;
  83. #endif
  84. }
  85. return msPtr;
  86. }
  87. };
  88.  
  89. }//namespace Blade
  90.  
  91. #endif // __Blade_Singleton_h__

再次更新:

blade中有类似这样的用法:

  1. //ClientConfig.h
  2. class ClientConfig : public Singleton<ClientConfig>
  3. {
  4. ...
  5. };
  6.  
  7. //main.cc
  8. static ClientConfig cfg(APP_NAME, APP_DESC, APP_VER);
  9.  
  10. int main()
  11. {
  12. return ;
  13. }
  14.  
  15. //xxx.cc
  16. ClientConfig.getSingleton().xxxx();

这么做只是为了灵活, 然而经过上面的改动以后, 这种方法已经不支持了. 所以又做了小的调整, 兼容这种初始化方式. 具体怎么写就不贴出来了, 应该比较简单.


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. 1 static T* msSingleton = NULL;

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

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

  1. 1 static T* msSingleton;

更正: 由于NULL是常量, 所以在编译期已经有值, 实际中可以直接编译到二进制镜像. 而C++对于常量初始化, 跟zero-initialization一样是最开始就有值的, 所以=NULL跟不写是一样的. 可能是时间太长记混了,Blade遇到的应该是msLock对象, 以及智能指针对象. 因为Lock不是常量构造, Lock()初始化函数会在程序加载以后, main之前调用. 这样msLock会有初始化顺序的问题, 只要对Lock制定特殊的初始化函数, 不做任何事情就可以了.

  1. //static locks maybe accessed/initialized by other pre-main routine(i.e. constructor of other static variables) before its own constructor
  2. //static constructor:DO NOT initialize members, to avoid reset data
  3. Lock(const int){}

这样即使Lock在使用以后调用intializer也没有问题了.

题外: 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, 或者initializer为空.

然而, 不同的singleton类内的静态变量msSingletonSP(smart pointer), 由于分布在不同的编译单元内, 其初始化顺序用户无法控制, 所以析构顺序根本无法控制,成了硬伤.

分析: 简单模式(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之前构造, 可以多加一个静态变量:

  1. class A : Singleton<A> {}
  2. class B
  3. {
  4. B()
  5. {
  6. A::getSingleton().xxxx();
  7. }
  8. ~B()
  9. {
  10. A::getSingleton().yyyy();
  11. }
  12. };
  13.  
  14. template <typename T> class SingletonInitializer
  15. {
  16. SingletonInitializer()
  17. {
  18. T::getSingleton();
  19. }
  20. };
  21.  
  22. static SingletonInitializer<A> c;
  23. 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

[原] blade中C++ singleton的实现的更多相关文章

  1. 面试中的Singleton

      引子 “请写一个Singleton.”面试官微笑着和我说. “这可真简单.”我心里想着,并在白板上写下了下面的Singleton实现: 1 class Singleton 2 { 3 public ...

  2. 引擎设计跟踪(九.14.2 final) Inverse Kinematics: CCD 在Blade中的实现

    因为工作忙, 好久没有记笔记了, 但是有时候发现还得翻以前的笔记去看, 所以还是尽量记下来备忘. 关于IK, 读了一些paper, 觉得之前翻译的那篇, welman的paper (http://gr ...

  3. C++面试中的singleton类

    引子 “请写一个Singleton.”面试官微笑着和我说. “这可真简单.”我心里想着,并在白板上写下了下面的Singleton实现: 1 class Singleton 2 { 3 public: ...

  4. Qt 中使用Singleton模式需小心

    在qt中,使用Singleton模式时一定要小心.因为Singleton模式中使用的是静态对象,静态对象是直到程序结束才被释放的,然而,一旦把该静态对象纳入了Qt的父子对象体系,就会导致不明确的行为. ...

  5. 在blade中定义一个可以被模版使用的变量

    laravel的blade中的数据一般由控制器传入,但是有没有什么办法临时在blade模版中创建并且被blade所使用吗? 答案是肯定的,不过语法稍微复杂一点 {{-- */$variableAvai ...

  6. Laravel 模板引擎Blade中标签详细介绍

    这篇文章主要介绍了Laravel模板引擎Blade中section的一些标签的区别介绍,需要的朋友可以来看看. Laravel 框架中的Blade模板引擎很好用,但是官方文档介绍的并不详细,我接下来将 ...

  7. C++primer原书中的一个错误(派生类using声明对基类权限的影响)

    在C++primer 第4版的 15章 15.2.5中有以下这样一段提示: "注解:派生类能够恢复继承成员的訪问级别,但不能使訪问级别比基类中原来指定的更严格或者更宽松." 在vs ...

  8. Java原子类中CAS的底层实现

    Java原子类中CAS的底层实现 从Java到c++到汇编, 深入讲解cas的底层原理. 介绍原理前, 先来一个Demo 以AtomicBoolean类为例.先来一个调用cas的demo. 主线程在f ...

  9. C#中实例Singleton

    [C#中实例Singleton] 1.经典方案: using System; public class Singleton { private static Singleton instance; p ...

随机推荐

  1. SVN客户端解决authorization failed问题

    遇到authorization failed问题 进人 [root@localhost conf]# pwd /opt/svndata/shell/conf [root@localhost conf] ...

  2. MySQL: InnoDB 还是 MyISAM?

    MyISAM存储引擎 MyISAM是 默认存储引擎.它基于更老的ISAM代码,但有很多有用的扩展.MyISAM存储引擎的一些特征:·      所有数据值先存储低字节.这使得数据机和操作系统分离.二进 ...

  3. 统计图表--第三方开源--MPAndroidChart(一)

    效果图1: 效果图2: MPAndroidChart是在Android平台上开源的第三方统计图表库,可以绘制样式复杂.丰富的各种统计图表,如一般常见的折线图.饼状图.柱状图.散点图.金融股票中使用的的 ...

  4. public void onItemClick(AdapterView arg0, View view, int position,long arg3)详解【整理自网络】

    参考自: http://blog.csdn.net/zwq1457/article/details/8282717 http://blog.iamzsx.me/show.html?id=147001 ...

  5. [重点翻译] ASP.NET 4.6的更新 -- 本文只摘录 Web Forms的部分

    原文出处:[重点翻译] ASP.NET 4.6的更新 -- 本文只摘录 Web Forms的部分 http://www.dotblogs.com.tw/mis2000lab/archive/2015/ ...

  6. 3.css中的颜色

    css中颜色的设置形式主要有三种方式:颜色名称.十六进制代码和十进制代码. 在古老的 HTML4 时,颜色名称只有 16 种. 颜色名称 十六进制代码 十进制代码 含义  black  #000000 ...

  7. 《Prism 5.0源码走读》 设计模式

    Prism或Prism构建的应用程序时会使用大量的设计模式,本文简要列举Prism相关的那些设计模式. Adapter(适配器模式):Prism Library主要在Region和IoC contai ...

  8. 如何在Eclipse中配置Tomcat

    1.Eclipse EE 配置Tomcat Eclipse EE 主要用于Java Web开发和J2EE项目开发.Eclipse EE中配置Tomcat比较简单,新建一个Tomcat Server即可 ...

  9. SQL1092N The requested command or operation failed because the user ID does not have the authority to perform the requested command or operation.

    1.前一天安装号db2后,做了如下处理: ************************************************************ 修改 /etc/sudoers 文件 ...

  10. 函数调用和inline作用

    函数调用的开销: 函数被调用时,要有函数调用和返回.要保存当前程序上下文信息,以便函数调用完毕后返回原来的地方,继续执行程序.将函数的参数进行压栈.出栈,执行函数,函数调用完毕后释放内部变量占用的内存 ...