您的位置:首页 > 编程语言 > C语言/C++

一个尽可能正确的Singleton实现 - c++描述

2013-10-25 21:09 330 查看

背景描述

说明: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++ 第三章]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐