0%

用C++写一个单例

“请用C++写一个单例,考虑一下多线程环境。”
这是一个常见的面试题,别人问过我,我也问过别人。
这个问题可以很简单,也可以很复杂。

简单有效的单例

1
2
3
4
5
6
7
class Singleton {
public:
static Singleton* GetInstance() {
Singleton singleton;
return &singleton;
}
};

在C++11中静态局部变量的初始化是线程安全的,参考链接
这种写法既简单,又是线程安全的,可以满足大多数场景的需求。

饿汉模式

单例在程序初期进行初始化。即如论如何都会初始化。

1
2
3
4
5
6
7
8
9
10
class Singleton {
public:
static Singleton* GetInstance() {
return singleton;
}
static Singleton* singleton;
};
Singleton* Singleton::singleton = new Singleton();

这种写法也是线程安全的,不过Singleton的构造函数在main函数之前执行,有些场景下是不允许这么做的。改进一下:

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
public:
static Singleton* GetInstance() {
return singleton;
}
int Init();
static Singleton* singleton;
};
Singleton* Singleton::singleton = new Singleton();

将复杂的初始化操作放在Init函数中,在主线程中调用。

懒汉模式

单例在首次调用时进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
public:
static Singleton* GetInstance() {
if (singleton == NULL) {
singleton = new Singleton();
}
return singleton;
}
static Singleton* singleton;
};
Singleton* Singleton::singleton = NULL;

这样写不是线程安全的。改进一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

在懒汉模式的基础上再改进一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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步:

  1. 申请内存
  2. 调用构造函数
  3. 将内存指针赋值给singleton

上面这个顺序是我们期望的,可以编译器并不会保证这个执行顺序。所以也有可能是按下面这个顺序执行的:

  1. 申请内存
  2. 将内存指针赋值给singleton
  3. 调用构造函数

这样就会导致其他线程可能获取到未构造好的单例指针。
解决办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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都不满足,所以返回引用更好一些。

结论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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!”

参考

The Singleton Pattern
面试中的Singleton
内存屏障