线程安全的C++ 单例
饿汉模式
必须在main开始前就初始化好,于是,需要一个静态变量。可以是对象,也可以是对象的指针。如果是指针,在main开始前new一下就行。由于这个静态变量只会被单例类使用,所以最好不是静态全局变量,而是类的静态成员变量。
// 对象版
// singleton.h
class Singleton {
private:
Singleton() {}
Singleton(const Singleton &);
Singleton& operator= (const Singleton &);
static Singleton s;
public:
static Singleton *GetInstance() {
return &s;
}
};
// singleton.cc
Singleton Singleton::s;
还有:
//指针版。没有析构逻辑,会造成泄露。
// singleton.h
class Singleton {
private:
Singleton() {}
Singleton(const Singleton &);
Singleton& operator= (const Singleton &);
static Singleton *p;
public:
static Singleton *GetInstance() {
return p;
}
};
// singleton.cc
Singleton *Singleton::p = new Singleton;
优点:
线程安全
实现简单,容易维护
缺点:
不适合部分场景。如:因为性能问题,希望懒加载;需要运行时才能知道,是否生成实例
由于在main开始前就必须初始化,几乎不可能给类传入任何参数。
懒汉模式
1.返回引用
使用局部静态变量。局部静态变量的初始化是线程安全的,这一点由编译器保证(http://gcc.gnu.org/ml/gcc-patches/2004-09/msg00265.html,这是一个GCC的patch,专门解决这个问题)。会在程序退出的时候自动销毁。
见: http://stackoverflow.com/questions/270947/can-any-one-provide-me-a-sample-of-singleton-in-c/271104#271104
适合C++11,保证静态局部变量的初始化是线程安全的。如果是C++98就不能用这个方法。
class S
{
public:
static S& getInstance()
{
static S instance;
return instance;
}
private:
S() {}
S(S const&); // Don't Implement.
void operator=(S const&); // Don't implement
};
加锁。线程安全,但每次都有开销。
// singleton.h
class Singleton {
public:
static Singleton *GetInstance() {
lock();
if (p == NULL) {
p = new Singleton;
}
return p;
}
private:
static Singleon *p;
Singleton() {}
Singleton(const Singleton &);
Singleton& operator= (const Singleton &);
};
// singleton.cc
Singleton *Singleton::p = NULL;
pthread_once
陈硕推荐的做法
class Singleton {
public:
static Singleton *GetInstance() {
pthread_once(&ponce_, &Singleton::init);
return value_;
}
private:
Singleton() {}
Singleton(const Singleton &);
Singleton& operator= (const Singleton &);
static void init() {
value_ = new T();
}
static pthread_once_t ponce_;
static Singleton *value_;
};
pthread_once_t SIngleton::ponce_ = PTHREAD_ONCE_INIT;
Singleton* Singleton::value_ = NULL;
DCL
double check locking.只能用内存屏障,其他做法都是有问题的。
参见论文: http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
普通的double check之所以错,是因为乱序执行和多处理器下,不同CPU中间cache刷往内存并对其他CPU可见的顺序无法保障(cache coherency problem)。
Singleton<T> *p = new Singleton<T>;
,那么实际有3步:
1. 分配内存
2. 构造
3. 赋值给p
2和3的顺序是未定的(乱序执行!)。因此,如果直接赋值给p那么很可能构造还没完成。此时另一个线程调用GetInstance,在lock外面check了一下,发现p!=NULL,于是直接返回p,使用了未初始化完成的实例,跪了。
那么,如果用中间变量转一下呢?用tmp_p转了下以后,tmp_p赋值给p的时候,显然p指向的实例是构造完成了的。然而,这个tmp_p在编译器看来明显没什么用,会被优化掉。
最佳实践?
1.eager initialize
2.次次加锁,但每个线程缓存了返回的指针,调用一次有用缓存的指针即可。
3.pthread_once