一个尽可能正确的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初级版
  1. Foo* GetFooInstance() {  
        static Foo foo;  
        return &foo;  
    }

或者

Foo& GetFooInstance() {  
    static Foo foo;  
    return foo;  
}

这种实现是最原始的版本,有时在项目中也是见的最多的一个版本,在单线程程序,甚至在许多单核设备上问题都不是很大,但是遇到多核多线程时,也许就会跪了,是否会有问题还与Foo在构造函数中是否有额外的初始化有关,额外初始化就会花费时间,这样多线程访问到`GetFooInstance`时,也许会有可能会构造两次foo。这样就有了下面一个升级版。

singleton升级版
  1. 复制代码
    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++ 第三章]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值