在C++编程里,单例是比较常用的一个模式,因为单例用起来兼具C++面向对象性,C面向过程性的爽感,它还有两个很受欢迎的恶魔级特性,
1)不需关心创建时机(因为在进入main函数之前已经初始化了)
2)不需显式回收(因为退出main过程后自动回收全局对象)
 
但程序里有过多的单例,会影响程序的启动速度且增加内存占用,所以一般会采用懒单例
模式替代之,即在需要使用的时候创建它,如下所示:
 
template<class T>
class Singlton{
public:
    static T& GetInstance(){
        if(m_pInstance == NULL){
            m_pInstance = new T;
        }
        return *m_pInstance;
    }
private:
    static T * m_pInstance;
};
template<class T>
T* Singlton<T>::m_pInstance = NULL;
 
这个单例类虽然看起来简洁明了,非常优美(先忽略它会在构造或析构时会带来什么问题),
但由于单例本质上是一个全局对象,采用上述单例,在多线程下是非常有问题的:
1)当Thread1通过判断(红色部分)进入if语句块执行申请内存操作,由于new操作是非原子
     操作(非线程安全),且会切换到内核模式。
2)当Thread2进入if判断时,Thread1的内存分配还未完成(m_pInstance没被赋值,仍然
     为NULL),Thread2也会执行申请内存操作。
3)当两者完成内存申请操作后,都会去给m_pInstance赋值,这会导致有一方数据被冲掉,
     出现数据一致性问题。
 
好了,现在看一下线程安全的懒单例类(出自google源码):
 
template <typename Ty_>
class LazySingleton {
public:
    static Ty_& GetInstance(){
1:    while(me_ == NULL || me_ == (void*)-1){
2:        PVOID result = InterlockedCompareExchangePointer((PVOID*)&me_, -1,   NULL);
3:          if(*(PVOID*)&me_ == -1){
4:             Ty_* new_instance = new Ty_();
5:             InterlockedCompareExchangePointer((PVOID*)&me_, (PVOID)new_instance, -1);
            }
        }
6:        return *const_cast<Ty_*>(me_);
    }
private:
    static volatile Ty_ *me_;
};
template <typename Ty_>
volatile Ty_* LazySingleton<Ty_>::me_;
 
首先介绍一下上述类中两个生僻用法:
a)关键字volatile,这个网上很多说法,如果在内存某处读取一个变量到寄存器进行操作,
   如果变量有任何变化,都会立即反应到内存中,而不会驻留在缓存(cache)中。
b)函数PVOID __cdecl InterlockedCompareExchangePointer(PVOID volatile *Destination,
                                                                                 PVOID Exchange,
                                                                                 PVOID Comparand)
  是windows提供的一个比较并交换指针值的原子操作函数,它表示如果destination的值与Comparand值相等,则把Exchange的值赋给destination,如果不相等则什么都不做,
  该函数提供完全的内存栅栏(barrier)来保证内存操作有序。
 
分析该类的线程安全性:
1) 如果对象已经创建,则不会进入while语句,直接返回对象引用,这种情况不存在线程安全性,接下来分析对象未创建情况。
2) 对象未创建(即me_== NULL),Thread1进入while循环,它首先通过第2行原子操作,如果me_==NULL,则me_=-1,否则什么都不做,即me_如果等于-1或其他值,直接进入
    第3行,如果有多个线程都进入第2行对me_进行写操作不会出现多线程问题(此处是为了防止与第5步产生同步问题),如果Thread1执行完第2行,进入第3行之前,Thread2已经给me_
    赋值,则会直接返回me_对象引用。
3) 我们主要分析下第4行,第5行代码,Thread1进入第4行,执行new操作,如果此时没有其他线程并行,则会顺利进入第5行,交换后me_为new_instance地址而不再为-1,这样其
    它线程不再进入第3步,对象创建完成。如果有进程并行进入第4行,两个进程都创建了一个实例对象,但执行到第5行时,需要按序执行,最终只会有一个实例对象被赋值到me_,不会
    引起数据不一致性问题。
 
结束语:在日常开发中一般不会有人写这么精细的懒单例类,因为即使是线程不安全类出问题的概率也很低,但这个类反应出多线程情况下一些复杂的问题需要我们关注。
posted on 2012-10-22 10:04  广州一粟  阅读(249)  评论(0编辑  收藏  举报