多线程中的单件模式

多线程中的单件模式

评论请移步:http://zbm.xuanwo.tk/2011/02/singleton-in-multithread.html

单件模式可能是所有设计模式中最简单的一个了,但在C++中,尤其是还要支持多线程的话,要想写一个正确的实现却并不容易,不信请看:


  
  
  
  
 1  class  CSingleton
 2  {
 3  public :
 4   CSingleton()
 5   {
 6    _tprintf( _T( " CSingleton::Constructor: Before Sleep\n " ) );
 7    Sleep(  1000  );  //  不会改变逻辑, 但增大了问题出现的概率
 8    _tprintf( _T( " CSingleton::Constructor: After Sleep\n " ) );
 9   }
10    void  DoSomeThing()
11   {
12    _tprintf( _T( " CSingleton::DoSomeThing\n " ) );
13   }
14    static  CSingleton *  GetInstance()
15   {
16     static  CSingleton *  p  =  NULL;
17     if ( p  ==  NULL )
18     p  =   new  CSingleton();
19     return  p;
20   }
21  };
22  unsigned __stdcall thread(  void *  )
23  {
24   CSingleton *  p  =  CSingleton::GetInstance();
25   p -> DoSomeThing();
26    return   0 ;
27  }
28  int  _tmain(  int  argc, _TCHAR *  argv[] )
29  {
30    for int  i  =   0 ; i  <   3 ++ i )
31   {
32    uintptr_t t  =  _beginthreadex( NULL,  0 , thread, NULL,  0 , NULL );
33    CloseHandle( (HANDLE)t );
34   }
35   _getch();
36    return   0 ;
37  }

上面的单件实现在单线程中肯定是正确的,不过在多线程中的输出却如下:

1  CSingleton::Constructor: Before Sleep
2  CSingleton::Constructor: Before Sleep
3  CSingleton::Constructor: Before Sleep
4  CSingleton::Constructor: After Sleep
5  CSingleton::DoSomeThing
6  CSingleton::Constructor: After Sleep
7  CSingleton::DoSomeThing
8  CSingleton::Constructor: After Sleep
9  CSingleton::DoSomeThing

很明显,虽然我们想做个单件,但它却出现了多个实例(或一个实例被初始化了多次)。其原因是我们的实现根本没有考虑多线程,那下面的代码把创建实例的部分锁住是不是就行了呢?

 1  class  CCriSec : CRITICAL_SECTION
 2  {
 3  public :
 4   CCriSec()
 5   {
 6    Sleep(  1000  );  //  增大出问题的概率, 但不改变逻辑
 7    InitializeCriticalSection(  this  );
 8   }
 9    ~ CCriSec() { DeleteCriticalSection(  this  ); }
10    void  Enter()
11   {
12    EnterCriticalSection(  this  );
13   }
14    void  Leave() { LeaveCriticalSection(  this  ); }
15  };
16  //  
17  static  CSingleton *  GetInstance()
18  {
19    static  CSingleton *  p  =  NULL;
20    static  CCriSec  lock ;
21    lock .Enter();
22    if ( p  ==  NULL )
23    p  =   new  CSingleton();
24    lock .Leave();
25    return  p;
26  }
27  //  

运行一下,不管输出是什么,程序崩溃了。分析一下可以发现,这个例子中的我们确实控制好了对CSingleton实例的初始化,但这种控制却依赖于另一个静态变量(CCriSec的实例)的初始化,而这个新的静态变量导致了程序的崩溃,也就是说我们在解决问题的同时引入了新的问题。而且,在这种情况下,就算再引入多少个新的临界区也无济于事,因为对最外层的临界区的初始化总会有问题。

上面的例子的问题在于CCriSec是一种复杂的数据类型,所以对它的初始化总要到运行时才能完成,如果用整数这样简单的、能在编译期完成初始化的数据类型来做是不是可以呢?

1  static  CSingleton *  GetInstance()
2  {
3    static  CSingleton *  p  =  NULL;
4    static   volatile   long   lock   =   0 ;
5    if ( InterlockedCompareExchange(  & lock 1 0  )  ==   0  )
6    p  =   new  CSingleton();
7    return  p;
8  }

看起来好像没有问题,但运行一下却是下面的输出:

1  CSingleton::Constructor: Before Sleep
2  CSingleton::DoSomeThing
3  CSingleton::DoSomeThing
4  CSingleton::Constructor: After Sleep
5  CSingleton::DoSomeThing

也就是说DoSomeThing在构造函数返回之前已经被调用了,这显然也是错误的。其原因是我们忽略了“对象的创建时需要时间的”,把这个问题也修正一下,就是最终的正确实现了:

 1  static  CSingleton *  GetInstance()
 2  {
 3    static  CSingleton *  p  =  NULL;
 4    static   volatile   long   lock   =   0 ;
 5    if ( InterlockedCompareExchange(  & lock 1 0  )  !=   0  )
 6   {
 7     while lock   !=   2  )  //  等待对象创建完成
 8     Sleep(  0  );
 9     return  p;
10   }
11   p  =   new  CSingleton();
12    lock   =   2 ;
13    return  p;
14  }

本文采用的单件实现是函数内的静态变量,如果你采用其它方式,也会有类似问题。其实在我看来,单件模式是一个看起来简单、做对了很难(上面演示的是多线程中的问题,在具体的实践中还会遇到很多其他问题)、同时又没有太多实用价值的东西。

另外,从Windows Vista开始,微软提供了一种多线程下对象初始化的方法,有兴趣的可以中搜一下“INITONCE”,个人认为INITONCE有点完美的过头了,真正好玩又有用的是与它同时出现的“条件变量(condition variable)”。

你可能感兴趣的:(多线程中的单件模式)