理解Double-Checked Locking

2017-05-05  本文已影响0人  whosemario

传统的单例模式实现

class Singleton {
public:
    static Singleton* instance();
    ...
private:
    static Singleton* pInstance;
};

Singleton* Singleton::pInstance = 0;

Singleton* Singleton::instance() {
    if(pInstance == 0) {
        pInstance = new Singleton();
    }
    return pInstance;
}

在多线程环境下,这种写法会引起condition race。

多线程基本实现

class Singleton {
public:
    static Singleton* instance();
    ...
private:
    static Singleton* pInstance;
};

Singleton* Singleton::pInstance = 0;

Singleton* Singleton::instance() {
    Lock lock;      // 获取锁
    if(pInstance == 0) {
        pInstance = new Singleton();
    }
    return pInstance;
}   // 释放锁

上面这种写法可以解决多线程下的condition race的问题,但性能消耗也会很大

多线程double-check

class Singleton {
public:
    static Singleton* instance();
    ...
private:
    static Singleton* pInstance;
};

Singleton* Singleton::pInstance = 0;

Singleton* Singleton::instance() {
    if(pInstance == 0) {
        Lock lock;      // 获取锁
        if(pInstance == 0) {
            pInstance = new Singleton();
        }
    }   // 释放锁
    return pInstance;
} 

这种实现看似可以但问题很多。

pInstance = new Singleton()

一个Singleton的实例化其实包含三步

1. 为Singleton分配空间
2. 调用Singleton的构造函数
3. 将空间的地址赋值给pInstance

这样上面的代码就改造为先的形式:

 Singleton* Singleton::instance() {
    if(pInstance == 0) {
        Lock lock;      // 获取锁
        if(pInstance == 0) {
            pInstance =         // Step 3
                operator new (sizeof(Singleton));   // Step 1
            new (pInstance) Singleton;          // Step 2
        }
    }   // 释放锁
    return pInstance;
} 

最关键的是步骤2和3的顺序是未定义的!也就是说,pInstance可能先拿到一个没有调用构造函数的地址,此时另一个线程发现pInstance已经非空了,此时pInstance指向一个脏数据。

即使步骤2和3在编译优化后保证了顺序,对于多Process架构来说,内存同步,也会导致问题。因为在Process A上步骤2和3是有序的修改内存,但是在Process B上发现两个地方的内存修改顺序可能是相反的。

为了解决这个问题,我们使用内存屏障:

Singleton* Singleton::getInstance() { 
    Singleton* tmp = m_instance; 
    ...         // insert memory barrier 
    if (tmp == NULL) { 
        Lock lock; 
        tmp = m_instance; 
        if (tmp == NULL) { 
            tmp = new Singleton; 
            ...     // insert memory barrier 
            m_instance = tmp; 
        } 
    } 
    return tmp; 
}

C++11前--pthread_once

C++11前比较通用的方式是使用pthread_once来实现单例

class Singleton {
public:
    static Singleton* instance();
    static void init() {
        pIntance = new Singleton;
    }
    ...
private:
    static Singleton* pInstance;
    static pthread_once_t ponce_;
};

Singleton* Singleton::pInstance = 0;
pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;

Singleton* Singleton::instance() {
    pthread_once(ponce_, &Singleton::init);
    return pInstance;
} 

可以看一下pthread_once的实现

static int once_lock = LLL_LOCK_INITIALIZER;


int
__pthread_once (once_control, init_routine)
     pthread_once_t *once_control;
     void (*init_routine) (void);
{
  /* XXX Depending on whether the LOCK_IN_ONCE_T is defined use a
     global lock variable or one which is part of the pthread_once_t
     object.  */
  if (*once_control == PTHREAD_ONCE_INIT)
    {
      lll_lock (once_lock, LLL_PRIVATE);

      /* XXX This implementation is not complete.  It doesn't take
     cancelation and fork into account.  */
      if (*once_control == PTHREAD_ONCE_INIT)
    {
      init_routine ();

      *once_control = !PTHREAD_ONCE_INIT;
    }

      lll_unlock (once_lock, LLL_PRIVATE);
    }

  return 0;
}
strong_alias (__pthread_once, pthread_once)
hidden_def (__pthread_once)

看起来非常像double checked的逻辑,但是lll_lock加入了内存屏障。

C++11

基于C++11实现单例就更加简单了

class Singleton {
public:
    static Singleton* instance() {
        static Singleton inst;
        return &inst;
    }
    ...
};

References

  1. C++ and Perils of Double-Checked Locking - http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
  2. 浅析单例模式与线程安全(Linux环境c++版本) - http://blog.csdn.net/cgxrit/article/details/43741771
上一篇下一篇

猜你喜欢

热点阅读