[原] blade中C++ singleton的实现
最近看了龚大大KalyGE中的singleton, 觉得非常不错(C++中线程安全并且高效的singleton).
可惜blade的代码都是C++03的, 没有使用C++11的任何特性. 笔者对于singleton也有些经验, 不过由于业余写代码本来就时间不够(blade在6年内堆了近20W行有效代码), 所以笔记记得非常少. 最近几个月业余没有写代码了, 所以有时间把blade在开发中遇到的问题贴出来.
而C++11虽然很爽很方便, 但是目前还没有加入支持的打算.
为了方便分析, 列出两种方式的singleton实现:
简单实现 (local static object):
- template<typename T> class Singleton
- {
- static T& getSingleton()
- {
- static T instance;
- return instance;
- }
- };
复杂实现( double check lock):
- static T* msSingleton = NULL;
- static Lock msLock;
- T& getSingleton()
- {
- if (NULL == msSingleton )
- {
- ScopeLock lock(msLock);
- if (NULL == msSingleton )
- {
- msSingleton = new T;
- }
- }
- return *msSingleton;
- }
下面两种实现, 称为简单模式和复杂模式, 并基于C++03标准分析.
1.定义和限制
singleton顾名思义, 就是单例, 所以不能有多个实例, 也不能复制. 笔者曾在工作中就遇到过新手类似下面的复制:
- 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无法初始化, 将它的初始化放在构造函数里:
- Singleton
- {
- assert(msSingleton == NULL );
- msSingleton = static_cast<T*>(this);
- }
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了:
- T& getSingleton()
- {
- #if __cplusplus < 201103L //C++11 doesn't need manual sync
- Lock msLock; //spin lock is fast enough, NO NEED for double check, thus no need for memory barrier
- ScopedLock guard(msLock);
- #endif
- static T instance;
- return instance;
- }
当然也可以使用double check lock+memory barrier, 效率上或许会好一点点?
再加上下面构造/析构顺序的检查, 目前最新的代码整理如下(比下文新):
- /********************************************************************
- created: 2009/02/07
- filename: Singleton.h
- author: Crazii
- purpose: the Singleton pattern: lazy initialization mode
- *********************************************************************/
- #ifndef __Blade_Singleton_h__
- #define __Blade_Singleton_h__
- #include "BladeTypes.h" //NULL
- #include "Lock.h"
- #include "memory/BladeMemory.h"
- namespace Blade
- {
- template< typename T >
- class Singleton : public StaticAllocatable
- {
- private:
- Singleton(const Singleton&);
- Singleton& operator=(const Singleton&);
- static void* operator new(size_t);
- static void* operator new[](size_t);
- typedef T* pointer;
- public:
- /* @brief public reference access */
- static T& getSingleton()
- {
- //lazy initialization
- return *Singleton<T>::operateSingleton();
- }
- /* @brief public pointer access */
- static pointer getSingletonPtr()
- {
- return Singleton<T>::operateSingleton();
- }
- protected:
- Singleton()
- {
- }
- virtual ~Singleton()
- {
- Singleton<T>::operateSingleton(true);
- }
- /* @brief call to create instance */
- static T* operateSingleton(bool reset = false)
- {
- static Lock msLock; //spin lock is fast enough, NO NEED for double check, thus no need for memory barrier
- ScopedLock lock(msLock);
- static T* msPtr = NULL;
- #if BLADE_DEBUG
- static bool isCreated = false;
- #endif
- if( reset )
- {
- assert( msPtr != NULL );
- msPtr = NULL;
- return NULL;
- }
- if( msPtr == NULL )
- {
- #if BLADE_DEBUG
- //avoid repeat destroying & creating 2nd time.
- //this usually happens when singleton construction order need explicit control
- assert(isCreated == false);
- #endif
- //local static make sure it constructed right on first call
- static T msInstance;
- msPtr = &msInstance;
- #if BLADE_DEBUG
- isCreated = true;
- #endif
- }
- return msPtr;
- }
- };
- }//namespace Blade
- #endif // __Blade_Singleton_h__
再次更新:
blade中有类似这样的用法:
- //ClientConfig.h
- class ClientConfig : public Singleton<ClientConfig>
- {
- ...
- };
- //main.cc
- static ClientConfig cfg(APP_NAME, APP_DESC, APP_VER);
- int main()
- {
- return ;
- }
- //xxx.cc
- 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: 初始化
- class A : Singleton<A> {};
- class B
- {
- B()
- {
- A::getSingleton().xxxx();
- }
- };
- 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;
更正: 由于NULL是常量, 所以在编译期已经有值, 实际中可以直接编译到二进制镜像. 而C++对于常量初始化, 跟zero-initialization一样是最开始就有值的, 所以=NULL跟不写是一样的. 可能是时间太长记混了,Blade遇到的应该是msLock对象, 以及智能指针对象. 因为Lock不是常量构造, Lock()初始化函数会在程序加载以后, main之前调用. 这样msLock会有初始化顺序的问题, 只要对Lock制定特殊的初始化函数, 不做任何事情就可以了.
- //static locks maybe accessed/initialized by other pre-main routine(i.e. constructor of other static variables) before its own constructor
- //static constructor:DO NOT initialize members, to avoid reset data
- 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: 析构
然而再看下面的情况:
- class A: Singelton<A> {}
- class B: Singleton<B>
- {
- ~B()
- {
- A::getSingleton().xxxx();
- }
- };
B在析构时依赖了A, 所以必须要求A在B之前构造. 但是由于lazy init的原因, 方便嘛, 可能使用的时候就很随意, 导致没有控制初始化顺序, 那么析构顺序也不对了.
但对于简单模式的Singleton, 至少顺序还是能控制的:
- B::getSingleton().xxxx(); //lazy init of B
- ...
- A::getSingleton().xxxx(); //lazy init of A
把上面的代码改为:
- A::getSingleton(); //force (explicit) init of A,B: A must construct before B
- B::getSingleton();
- ...
- B::getSingleton().xxxx();
- ...
- A::getSingleton().xxxx();
- ...
这样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:
- T* msSingleton;
- T& getSingleton()
- {
- if (NULL == msSingleton )
- {
- static Lock msLock;
- ScopeLock lock(msLock);
- if (NULL == msSingleton )
- {
- static SMART_PTR<T> p;
- p = new T;
- msSingleton = p;
- }
- }
- return *msSingleton;
- }
由于local static smart pointer的存在, 就可以控制lazy de-init的顺序了. 这种模式对于问题a和问题b都可以解决顺序问题. 对于问题b, 因为初始化顺序比较随意, 除非事先仔细分析, 否则要等析构出问题了才能修复.
然而lazy init就是为了方便, 使程序员可以不管这些细节, 集中精力做业务逻辑. 所以等出了问题再改也不是不能接受. 至少析构顺序是可控制的, 而不像之前, 那么痛苦的根本不能控制顺序.
为了方便检测问题b, 不至于崩溃在深层调用后, 还要费神查看调用栈, 加上一个assert提前拦截到错误就可以了.
- T* msSingleton;
- ~Singleton()
- {
- msSingleton = NULL;
- }
- T& getSingleton()
- {
- if (NULL == msSingleton )
- {
- static bool isCreated = false;
- assert( isCreated == false ); //avoid create for the second time, this usually happens when singleton construction order need explicit control
- static Lock msLock;
- ScopeLock lock(msLock);
- isCreated = true;
- if (NULL == msSingleton )
- {
- static SMART_PTR<T> p;
- p = new T;
- msSingleton = p;
- }
- }
- return *msSingleton;
- }
在这种情况下, 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使用的方案.
- //note: DO NOT specify a initializer for it
- //Otherwise some early lazy initialization will be overwritten by initializer.
- T* msSingleton;
- Singleton()
- {
- assert(msSingleton == NULL );
- msSingleton = static_cast<T*>(this);
- }
- ~Singleton()
- {
- msSingleton = NULL;
- }
- T& getSingleton()
- {
- if (NULL == msSingleton )
- {
- static bool isCreated = false;
- assert( isCreated = false ); //avoid create for the second time, this usually happens when singleton construction order need explicit control
- static Lock msLock;
- ScopeLock lock(msLock);
- isCreated = true;
- if (NULL == msSingleton )
- {
- static volatile T p;
- assert( msSingleton == &p );
- }
- }
- return *msSingleton;
- }
如果是local static smart pointer, 那么需要动态分配, 由于blade已经有static memory pool, 所以这里也用上了, 将singleton 对象占用的内存, 放在static memory pool中. 不过缺点是, 这样让singleton的实现变得又更加复杂了...
虽然看起来并不是那么复杂:
- template<typename T> class Singleton : public StaticAllocatable
- {
- ...
- };
不过前面也提到了, 即便使用local static object, 但是singleton也有可能是new出来的, 如果这种情况允许, 虽然情况不多, 加上内存管理会更好. 这样即便使用smart pointer也没有问题了.
singleton的内存管理思路, 是基于生命周期的内存管理, 笔者在另一个博客也简单提了思路:
显然static memory不是为singleton专门设计的, 它的用处也不止于此, 比如游戏的database, 等等各种静态数据, 都可以放入static pool中去. singleton只是享受了一下便利罢了.
还比如对应的还有标准容器扩展, 只要实现了符合std标准的allocator就可以了. 比如staticvector, staticmap static set等等, 这些都可以作为singleton object的成员变量来使用.
不过内存管理是另一个巨话题, 这里不能多说了, 否则又将是另一个长篇大论了.
6.抽象单例
[原] blade中C++ singleton的实现的更多相关文章
- 面试中的Singleton
引子 “请写一个Singleton.”面试官微笑着和我说. “这可真简单.”我心里想着,并在白板上写下了下面的Singleton实现: 1 class Singleton 2 { 3 public ...
- 引擎设计跟踪(九.14.2 final) Inverse Kinematics: CCD 在Blade中的实现
因为工作忙, 好久没有记笔记了, 但是有时候发现还得翻以前的笔记去看, 所以还是尽量记下来备忘. 关于IK, 读了一些paper, 觉得之前翻译的那篇, welman的paper (http://gr ...
- C++面试中的singleton类
引子 “请写一个Singleton.”面试官微笑着和我说. “这可真简单.”我心里想着,并在白板上写下了下面的Singleton实现: 1 class Singleton 2 { 3 public: ...
- Qt 中使用Singleton模式需小心
在qt中,使用Singleton模式时一定要小心.因为Singleton模式中使用的是静态对象,静态对象是直到程序结束才被释放的,然而,一旦把该静态对象纳入了Qt的父子对象体系,就会导致不明确的行为. ...
- 在blade中定义一个可以被模版使用的变量
laravel的blade中的数据一般由控制器传入,但是有没有什么办法临时在blade模版中创建并且被blade所使用吗? 答案是肯定的,不过语法稍微复杂一点 {{-- */$variableAvai ...
- Laravel 模板引擎Blade中标签详细介绍
这篇文章主要介绍了Laravel模板引擎Blade中section的一些标签的区别介绍,需要的朋友可以来看看. Laravel 框架中的Blade模板引擎很好用,但是官方文档介绍的并不详细,我接下来将 ...
- C++primer原书中的一个错误(派生类using声明对基类权限的影响)
在C++primer 第4版的 15章 15.2.5中有以下这样一段提示: "注解:派生类能够恢复继承成员的訪问级别,但不能使訪问级别比基类中原来指定的更严格或者更宽松." 在vs ...
- Java原子类中CAS的底层实现
Java原子类中CAS的底层实现 从Java到c++到汇编, 深入讲解cas的底层原理. 介绍原理前, 先来一个Demo 以AtomicBoolean类为例.先来一个调用cas的demo. 主线程在f ...
- C#中实例Singleton
[C#中实例Singleton] 1.经典方案: using System; public class Singleton { private static Singleton instance; p ...
随机推荐
- SVN客户端解决authorization failed问题
遇到authorization failed问题 进人 [root@localhost conf]# pwd /opt/svndata/shell/conf [root@localhost conf] ...
- MySQL: InnoDB 还是 MyISAM?
MyISAM存储引擎 MyISAM是 默认存储引擎.它基于更老的ISAM代码,但有很多有用的扩展.MyISAM存储引擎的一些特征:· 所有数据值先存储低字节.这使得数据机和操作系统分离.二进 ...
- 统计图表--第三方开源--MPAndroidChart(一)
效果图1: 效果图2: MPAndroidChart是在Android平台上开源的第三方统计图表库,可以绘制样式复杂.丰富的各种统计图表,如一般常见的折线图.饼状图.柱状图.散点图.金融股票中使用的的 ...
- 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 ...
- [重点翻译] ASP.NET 4.6的更新 -- 本文只摘录 Web Forms的部分
原文出处:[重点翻译] ASP.NET 4.6的更新 -- 本文只摘录 Web Forms的部分 http://www.dotblogs.com.tw/mis2000lab/archive/2015/ ...
- 3.css中的颜色
css中颜色的设置形式主要有三种方式:颜色名称.十六进制代码和十进制代码. 在古老的 HTML4 时,颜色名称只有 16 种. 颜色名称 十六进制代码 十进制代码 含义 black #000000 ...
- 《Prism 5.0源码走读》 设计模式
Prism或Prism构建的应用程序时会使用大量的设计模式,本文简要列举Prism相关的那些设计模式. Adapter(适配器模式):Prism Library主要在Region和IoC contai ...
- 如何在Eclipse中配置Tomcat
1.Eclipse EE 配置Tomcat Eclipse EE 主要用于Java Web开发和J2EE项目开发.Eclipse EE中配置Tomcat比较简单,新建一个Tomcat Server即可 ...
- 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 文件 ...
- 函数调用和inline作用
函数调用的开销: 函数被调用时,要有函数调用和返回.要保存当前程序上下文信息,以便函数调用完毕后返回原来的地方,继续执行程序.将函数的参数进行压栈.出栈,执行函数,函数调用完毕后释放内部变量占用的内存 ...