Prodesse Quam Conspici

thread-safe singleton in c++


C++中实现一个线程安全的单例模式(Singleton),并不如看起来那么简单。最近刚好听了这么一个分享,阅读原始文献[1]后写一个简单的记录。

从一个简单的实现开始。

class Singleton {
public:
  static Singleton* instance();

private:
  static Singleton* pInstance;
};

Singleton* Singleton::pInstance = NULL;

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

这个实现展示了Singleton的大体代码结构,为保证只有一个实例,ctor设成private,提供一个static的instance函数获取static member。在instance第一次被调用时完成static member的构造。这个实现的缺点很显然,它未考虑线程安全性的问题。两个线程可能同时判断并发现pInstance==NULL,然后都去new Singleton,导致内存泄露。

1 thread-safe singleton

不难修改以上代码使其满足线程安全性要求。

Singleton* Singleton::instance() {
  Lock lock; // acquire lock
  if (pInstance == NULL) {
    pInstance = new Singleton();
  }
  return pInstance;
} // release lock

这样Singleton是线程安全的了,每次只有一个线程判断是否需要new Singleton,但这个实现每次调用instance都要lock,对于已经new过的Singleton显然是没有必要的。

2 double-check locking

为解决上面一个实现的性能问题,采用一种称为double-check locking pattern的办法,即在locking之前先检查一次pInstance==NULL,如果否,则无须lock,否则lock之后再判断是否确实需要new。

Singleton* Singleton::instance() {
  if (pInstance != NULL) // 1st check
    return pInstance;

  Lock lock; // acquire lock
  if (pInstance == NULL) { // 2nd check
    pInstance = new Singleton();
  }
  return pInstance;
} // release lock

3 perils of DCLP

DCLP看起来完美解决了我们的问题,但其实DCLP不是线程安全的,这是最神奇的地方。

pInstance = new Singleton();

这行代码实际包含了以下三个动作:

tmp=operator new(sizeof(Singleton)); // step 1
new(pInstance)Singleton;             // step 2
pInstance=tmp;                       // step 3

但是,编译器可能reorder step 2 and step 3,DCLP的代码看起来像这样:

Singleton* Singleton::instance() {
  if (pInstance != NULL) // 1st check
    return pInstance;

  Lock lock; // acquire lock
  if (pInstance == NULL) { // 2nd check
    Singleton* tmp=operator new(sizeof(Singleton)); // step 1
    pInstance=tmp;                       // step 3
    new(pInstance)Singleton;             // step 2
  }
  return pInstance;
} // release lock

先进入critical section的线程执行完step 1后,并发线程在1st check时发现pInstance!=NULL,于是return pInstance,但实际上pInstance只是一段裸内存而已,尚未构造,从而导致core。

4 thread safe singleton

引入memory barrier可以解决reordering的问题,代码看起来像这样:

Singleton* Singleton::instance() {
  if (pInstance != NULL) // 1st check
    return pInstance;

  Lock lock; // acquire lock
  if (pInstance == NULL) { // 2nd check
    Singleton* tmp = new Singleton();
    MemoryBarrier();
    pInstance = tmp;
  }
  return pInstance;
} // release lock

MemoryBarrier要求前后的代码不能越过memory barrier,所以tmp指向的一定是已经构造好的singleton。

5 Meyers' Singleton Pattern

由于C++11中规定并发线程必须等待变量初始化完成,所以线程安全的singleton实现就很简单了。

Singleton& Singleton::instance() {
  static Singleton ins;
  return ins;
}

References

[1] S. Meyers, A. Alexandrescu, C++ and the perils of double-checked locking, Dr Dobb’s Journal-Software Tools for the Professional Programmer. (2004) 57–61.

Tags: coding.