“请用c 写一个单例,考虑一下多线程环境。”
这是一个常见的面试题,别人问过我,我也问过别人。
这个问题可以很简单,也可以很复杂。
简单有效的单例
class singleton {
public:
static singleton* getinstance() {
singleton singleton;
return &singleton;
}
};
在c 11中静态局部变量的初始化是线程安全的,。
这种写法既简单,又是线程安全的,可以满足大多数场景的需求。
饿汉模式
单例在程序初期进行初始化。即如论如何都会初始化。
class singleton {
public:
static singleton* getinstance() {
return singleton;
}
static singleton* singleton;
};
singleton* singleton::singleton = new singleton();
这种写法也是线程安全的,不过singleton的构造函数在main函数之前执行,有些场景下是不允许这么做的。改进一下:
class singleton {
public:
static singleton* getinstance() {
return singleton;
}
int init();
static singleton* singleton;
};
singleton* singleton::singleton = new singleton();
将复杂的初始化操作放在init函数中,在主线程中调用。
懒汉模式
单例在首次调用时进行初始化。
class singleton {
public:
static singleton* getinstance() {
if (singleton == null) {
singleton = new singleton();
}
return singleton;
}
static singleton* singleton;
};
singleton* singleton::singleton = null;
这样写不是线程安全的。改进一下:
class singleton {
public:
static singleton* getinstance() {
lock();
if (singleton == null) {
singleton = new singleton();
}
unlock();
return singleton;
}
static singleton* singleton;
};
singleton* singleton::singleton = null;
这样写虽是线程安全的,但每次都要加锁会影响性能。
dclp(double-checked locking pattern)
在懒汉模式的基础上再改进一下:
class singleton {
public:
static singleton* getinstance() {
if (singleton == null) {
lock();
if (singleton == null) {
singleton = new singleton();
}
unlock();
}
return singleton;
}
static singleton* singleton;
};
singleton* singleton::singleton = null;
两次if判断避免了每次都要加锁。但是,这样仍是不安全的。因为”singleton = new singleton();”这句不是原子的。
这句可以分为3步:
- 申请内存
- 调用构造函数
- 将内存指针赋值给singleton
上面这个顺序是我们期望的,可以编译器并不会保证这个执行顺序。所以也有可能是按下面这个顺序执行的:
- 申请内存
- 将内存指针赋值给singleton
- 调用构造函数
这样就会导致其他线程可能获取到未构造好的单例指针。
解决办法:
class singleton {
public:
static singleton* getinstance() {
if (singleton == null) {
lock();
if (singleton == null) {
singleton* tmp = new singleton();
memory_barrier(); // 内存屏障
singleton = tmp;
}
unlock();
}
return singleton;
}
static singleton* singleton;
};
singleton* singleton::singleton = null;
语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。简单的说就是保证指令一定程度上的按顺序执行,避免上述所说的乱序行为。
把单例写成这么复杂也是醉了。
返回指针还是引用?
singleton返回的实例的生存期是由singleton本身所决定的,而不是用户代码。我们知道,指针和引用在语法上的最大区别就是指针可以为null,并可以通过delete运算符删除指针所指的实例,而引用则不可以。由该语法区别引申出的语义区别之一就是这些实例的生存期意义:通过引用所返回的实例,生存期由非用户代码管理,而通过指针返回的实例,其可能在某个时间点没有被创建,或是可以被删除的。但是这两条singleton都不满足,所以返回引用更好一些。
结论
class singleton {
public:
static singleton& getinstance() {
static singleton singleton;
return singleton;
}
// 如果需要有比较重的初始化操作,则在安全的情况下初始化
int init();
private:
// 禁用构造函数、拷贝构造函数、拷贝函数
singleton();
singleton(const singleton&);
singleton& operator=(const singleton&);
};
这种写法比较简单,可以满足大多数场景的需求。如果不能满足需求,再考虑dclp那种复杂的模式。如《unix编程艺术》中所说:“keep it sample, stupid!”