SingleTon模式的多线程环境


单实例模式也会在多线程环境下应用,所以也要让单实例模式适应多线程环境的应用;

先看一个单实例模式类的定义:
class SingleTon
{
  private:
    static SingleTon* m_lpInstance;

  private:
    SingleTon(void);
 
  public:
    ~SingleTon(void);

    static SingleTon& Instance(void);
};

SingleTon& SingleTon::Instance(void)
{
  if(!(this->m_lpInstance))                 //1;
  {
    this->m_lpInstance = new SingleTon();   //2;
  }
  return *(this->m_lpInstance);             //3;
}

我们对这个类在多线程环境下的调用情况做一个分析,假设有两个线程来访问Instance()方法:
第一个线程进入Instance()函数中,并检测if条件.由于这是第一次访问,所以m_lpInstance为NULL,于是进入//2这一行,准备调用new操作符创建创建新对象.但是此时操作系统的任务调度器可能会中断这个线程的活动,从而把CPU的控制权转给第二个线程;于是第二个线程也来调用Instance()方法,并发现此时的m_lpInstance仍然为NULL,因为第一个线程还没有来得及修改它,到目前为止,第一个线程只是完成了对m_lpInstance是否为NULL的测试.现在假设第二个线程已经完成了对new操作符的调用,并顺利地完成对象的创建和m_lpInstance的复制操作,并返回它;
不幸的是,当第一个线程再次被调度的时候,它只记得它应该去执行第//2行代码,并因此再次创建对象,并把返回的指针再次复制给m_lpInstance,并返回它.这样,程序的内存堆中就有了两个SingleTon对象,其中一个必定是内存泄露造成的.每个线程都各自拥有一个指向SingleTon对象的指针,但是它们指向的是同一个SingleTon对象,而另外一个指针对象的内存则丢失了,这样肯定会让程序步入混乱.如果多个线程同时访问Instance()方法,后果不堪设想;
这就是所说的"竞态条件";
SingleTon对象是共享的全局资源.而所有共享的全局资源对竞态条件和多线程环境而言都是不可靠的、不安全的;
有一种方法可以消除这种竞态条件,那就是要说的"双检测锁定"模式;
下面的改善版的Instance()方法虽然可以消除竞态条件,但是并不是最完美的实现:
SingleTon& SingleTon::Instance(void)
{
  pthread_mutex_lock(&m_lock);    //加锁
  if(!(this->m_lpInstance))
  {
    this->m_lpInstance = new SingleTon();
  }
  pthread_mutex_unlock(&m_lock);  //解锁
  return *(this->m_lpInstance);
}
当某个线程对m_lpInstance赋值的时候,其它线程必定阻塞于pthread_mutex_lock(&m_lock)调用加锁处.当另外一个线程对m_lock加锁的时候,它会发现pthread_mutex_lock(&m_lock)已经被初始化,这个方法比较好;
这种方法虽然好,但是它的不足之处是缺乏效率:每次调用Instance()时都会执行加锁和解锁操作,即使是竞态条件只出现一次.加锁和解锁操作的代价都很昂贵;下面的Instance()方法的实现可以缓解额外开销:
SingleTon& SingleTon::Instance(void)
{
  if(!(this->m_lpInstance))
  {
    pthread_mutex_lock(&m_lock);    //加锁
    this->m_lpInstance = new SingleTon();
    pthread_mutex_unlock(&m_lock);  //解锁
  }
  return *(this->m_lpInstance);
}
现在,额外开销不见了,但是竞态条件又回来了.第一个线程通过了if条件测试,但是正当它准备进入"同步代码区段"时,操作系统调度器中断了这个线程,并把CPU的控制权转给第二个线程.第二个线程通过了if条件测试并步入同步代码区段,而后结束执行.当第一个线程重新被调度的时候,它也进入了同步代码区段,但为时已晚,程序中于是又构造出两个SingleTon对象.其中一个必定是内存泄露造成的;
最完美的方法就是"双检测锁定":方法很简单,首先进行条件测试,然后进入同步代码区段,然后再次进行if条件检测.但是第二次检测时,指针要么已经被初始化,要么就是还没有被初始化;请看具体实现:
SingleTon& SingleTon::Instance(void)
{
  if(!this->m_lpInstance)                      //第一次检测;   //1 模糊区
  {                                                           //2 模糊区
    pthread_mutex_lock(&m_lock);              //加锁          //3 非模糊区
    if(!(this->m_lpInstance))                 //第二次检测;   //4 非模糊区
    {
      this->m_lpInstance = new SingleTon();
    }
    pthread_mutex_unlock(&m_lock);            //解锁
  }
  return *(this->m_lpInstance);
}
这样做,就确保万无一失了;
当第一个线程通过if条件检测(第//1行)之后,到达第//2行,这一行是个模糊控制区,这个时候操作系统调度器中断了第一个线程的CPU控制权,并把CPU的控制权转交给第二个线程,第二个线程也通过了第//1行的if条件检测并到达模糊控制区第//2行,然后进入第//3行,获得对同步对象m_lock的枷锁,枷锁成功之后,模糊控制不复存在了,一切都变得非常清晰:同一时刻只允许有一个线程进入,要么已经完全初始化,要么根本没有被初始化.第一个进入第//3行的线程会再次通过第//4行的if条件检测,创建对象并初始化指针变量m_lpInstance,此时此刻,其它线程正阻塞在第//3行,无法进入第//4行,知道第一个进入第//4的线程完成初始化并解锁,下一个线程才可以进入,但为时已晚,指针变量m_lpInstance已经被完全初始化,不用再次初始化了;其余所有线程都会在第//4行的第二次条件检测行动中失败,因而不会产生任何东西;第//1行和第//2行是模糊区,第//3行和第//4行是非模糊区;
第一次条件检测快速而粗糙:如果SingleTon对象存在,你可以得到它,不需要创建,否则就要做进一步的检测.
第二次条件检测缓慢而精确:它判断SingleTon对象是否确实已经被初始化,如果没有,这个线程就会负责对它进行初始化.
这就是"双检测锁定"模式;即具备了应有的访问效率,也消除了竞态条件;
"双检测锁定"模式虽然是这样完美,但是仍然还有它的不足之处;真正应用的时候,它并不总是正确的:RISC(精简指令集)机器的编译器有一个所谓的"代码重新排列"机制,它会重新排列编译器所产生出来的汇编语言指令,使代码能够最佳运用RISC处理器的平行特性;比如:RISC机器可以同时执行一个load动作和一个add动作;
"重新排列指令"是RISC处理器能够达到优化的一个主要因素,它甚至可以使速度加倍.但是它也有可能破坏"双检测锁定"模式:编译器有可能在锁定m_lock之前先执行第二个if条件测试,于是竞态条件再度出现.如果希望你产生出来的代码总是"政策上正确",那会剧烈降低所有代码的速度;
方法是:实现"双检测锁定"模式之前,先查阅对应的编译器说明文档.通常系统平台会提供选择性的、不可移植的并发基本元素,比如"内存屏障",那是一种轻型的mutex,至少你应该在m_lpInstance指针变量的声明之前添加修饰词volatile,因为合理的编译器会为volatile对象产生出恰当而明确的代码;


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值