一个尽可能正确的Singleton实现 - c++描述
背景描述
说明:singleton有各式各样的翻译,因人口味而异,所以,本文中对singleton就直接用英文了。
singleton的实现方式也多多,以下只是做一种非侵入的使用double-checked locking方式进行描述,有错误的地方尽情拍砖。
在整个软件开发中,singleton应该是使用最广泛的一种设计模式,也几乎每个人都写过若个版本的单例实现,在这些singleton实现中,很大一部分实现均是有问题的,甚至是错误。以下从两个方面进行说明为什么,第一:singleton的生命周期问题,如:有A、B两个singleton,初始化顺序为A、B,析构顺序当然为B、A,但是在A析构时调用了B,这时候将会产生什么结果? 第二:C++11以前语言本身没有提供内存边界(memory barrier)这个东西(其他语言也会有这个问题),C++的内存模型就不支持内存边界,导致在编译器优化或者cpu乱序(out-of-order)执行时,产生了非预期的结果,且该现象很难复现。
如果您已经熟练使用singleton我建议您直接跳到singleton高级版,查看最后c++11带来的memory barrier给C++程序猿带来的福利。
singleton初级版
Foo* GetFooInstance() { static Foo foo; return &foo; }
或者
Foo& GetFooInstance() { static Foo foo; return foo; }
这种实现是最原始的版本,有时在项目中也是见的最多的一个版本,在单线程程序,甚至在许多单核设备上问题都不是很大,但是遇到多核多线程时,也许就会跪了,是否会有问题还与Foo在构造函数中是否有额外的初始化有关,额外初始化就会花费时间,这样多线程访问到`GetFooInstance`时,也许会有可能会构造两次foo。这样就有了下面一个升级版。
singleton升级版
Foo* GetFooInstance() { static Foo* foo = NULL; if (NULL == foo) { foo = new Foo; // (1) } return foo; }
这次升级后与上次相差不大,但是可以在(1)后面干点额外的东西了,如二段初始化等,但是还是没避免多线程问题。再次升级一下。
template <typename T> class DefaultAllocatorTraits { public: static T* Allocate() { return new T; } static void Delete(T* t) { typedef char type_must_be_complete[ sizeof(T)? 1: -1 ]; delete t; } }; template <class T, class allocator = DefaultAllocatorTraits<T> > class Singleton { public: Singleton() : singleton_(NULL) { InitializeCriticalSection(&wait_event_); } ~Singleton() { DeleteCriticalSection(&wait_event_); allocator::Delete(singleton_); } T* GetInstance() { // (1) if (NULL == singleton_) { // (2) EnterCriticalSection(&wait_event_); // (3) if (NULL == singleton_) { singleton_ = allocator::Allocate(); } LeaveCriticalSection(&wait_event_); } return singleton_; } private: CRITICAL_SECTION wait_event_; T* singleton_; };
以上代码使用方式为
static Singleton<Foo> foo; foo.GetInstance();
代码看似很长,只是想看清所有内容而已,全部内容都在GetInstance中了,这里面的代码是以windows为例的,为了示例就没有封装锁了,意思一下即可,锁的位置可以放到(1)处,这时候呢每次进入到这个函数里面都得加一次锁,虽然没什么问题,但是消耗挺大的,所以放到(2)处,当获取到临界区的使用权限后,在对singleton_ 进行一次检查,并申请内存内存初始化等,这里进行二段初始化、三段初始化都已经没有问题了,这就是所谓的“Double-Checked Locking”。以下简称DCL。
DCL解决了大部分情况下遇到的多数问题,如碰到多线程,即使两个线程都进入到二的位置,A线程获得临界区,进入到(3)然后执行完毕后退出临界区,B线程再进入,这时候由于检查了singleton_的有效性,所以不会再次初始化,以后都将不会进入到(2),所以这个版本的singleton基本能够投入使用了。
但是这也不是没问题的,这里的singleton只负责了申请内存,没有释放,当然整个程序都有效的,有无释放都无所谓了。不过没释放总感觉少点啥,不是吗?
singleton高级版
传说中,C提供了一个高端的API`[atexit](http://www.cplusplus.com/reference/cstdlib/atexit/)`,这个东西呢能让我们注册一个在程序退出时调用函数的方法,在析构之前,也挺好,如果看着不爽或者不太想用这个时,可以自行实现,chrome的base库里面提供了一个实现[AtExit](http://src.chromium.org/viewvc/chrome/trunk/src/base/at_exit.h?revision=148405),这个都是小事,问题是这个能够辅助我们解决内存释放问题了,修改上面的单例,在申请内存的地方注册一下退出接口。我这里就使用的是`AtExitManager`因为C库那玩意儿不带参数,不能满足我的这个需求。修改如下
T* GetInstance() { if (NULL == singleton_) { EnterCriticalSection(&wait_event_); if (NULL == singleton_) { singleton_ = memory_traits::Allocate(); AtExitManager::RegisterCallback(Singleton::OnExit, this); } LeaveCriticalSection(&wait_event_); } return singleton_; } static void OnExit(void* singleton) { Singleton<T, allocator>* me = reinterpret_cast<Singleton<T, allocator>*>(singleton); allocator::Delete(me->singleton_); }
实现还是比较粗糙的,但是至少能够初始化,能够释放了,不考他自身自灭了。
到了现在,还是没能解决我的目的,重新初始化及[乱序执行](http://en.wikipedia.org/wiki/Out-of-order_execution)的问题。甚至有时候编译器优化的recorder等一系列的问题,都有可能导致代码并不是按照人想的一样执行
如
T* GetInstance() { // (1) if (NULL == singleton_) { // (2) EnterCriticalSection(&wait_event_); // (3) if (NULL == singleton_) { // (4) singleton_ = allocator::Allocate(); } LeaveCriticalSection(&wait_event_); } return singleton_; }
这段代码中,(3)、(4)处的内容被out-of-order执行的cpu弄到前面去执行的话,产生的问题将不可预料,当然这种情况也极少。
在C++11修改过多线程的内存模型后,添加了memory barrier的东西,使指令在指定的一段内存中不会被使用乱序执行,当然memory barrier这个东西不是所有平台都支持的。有了这个之后修改上面的内容,(注:在java5.1以前的内存模型也会有这个问题,至于现在是否有修改不知道。。。求答案)。
T* GetInstance() { std::atomic_thread_fence(std::memory_order_acquire); if (NULL == singleton_) { EnterCriticalSection(&wait_event_); if (NULL == singleton_) { singleton_ = allocator::Allocate(); } LeaveCriticalSection(&wait_event_); } std::atomic_thread_fence(std::memory_order_release); return singleton_; }
至于`memory_order_release`放到`singleton_ = allocator::Allocate()`后面也没太大关系。
有了这么一玩意儿,在fence之间的代码就不会乱序执行了,当然需要编译器支持才能编译的过,详情参见[cplusplus.com](http://www.cplusplus.com/reference/atomic/atomic_thread_fence/?kw=atomic_thread_fence).
注:linux平台提供了一些barrier可以使用,也能解决该问题。
最后一个问题,单例牺牲后的恢复,这个问题,在前面提供析构函数的地方基本已经得到解决,有了析构函数之后,对原有变量稍加改动即可。
因为,使用AtExitManager来控制生命周期后,按如下方式使用:
int main() { AtExitManager exit_manager; // other operations. return 0; }
exit_manager的生命周期受main控制,所以呢,肯定先于其他静态变量析构,在`exit_manager`退出时就会一一将单例析构,即使出现A B B A,A在调用B的情况,也会重新初始化单例B,因为静态变量还没析构,继续注册,继续析构,所以这种问题得以解决。
到这里实现的单例,应该来说还是比较完善了。可以尽情的happy了。
### lock free ###
前面的所有实现中均采用了锁的机制,现在应用的也挺多,但是编程难度挺高的一个lock free,可以让减少锁的使用,同时还可以提高性能,由于这个话题没能具体衡量,所以只好简要一笔带过。留作以后完善。
直接参考chrome的源码也就是讲前面的锁换成一个spin lock,采用原子操作,减少整体线程切换及切换到内核状态等一系列的开销。
参考:
http://src.chromium.org/viewvc/chrome/trunk/src/base/lazy_instance.h?revision=204953
这里面的lazy_instance就可以稍加修改拿来使用。
结论
最终我们使用DCL的方式实现了一个近乎完善的singleton,但是这只是其中的一种方法,如:c++11提供了一个方法[call_once](http://en.cppreference.com/w/cpp/thread/call_once),这个一看名字,对于单例来说太爽了。方法多多。
### 参考 ###
- Double-Checked Locking is Broken
- C++ and the Perils of Double-Checked Locking by scott meyers and andrei alexandrescu
- Double-Checked Locking Is Fixed In C++11
- chrome lazy_instance
- [api design for c++ 第三章]