从单例模式谈起(一)

单例模式可能是大家最为熟知的一种设计模式,本身没什么好谈的。但是在 C++ 中,由单例模式可以引出一系列的问题,可能会比较有意思,这里探讨一下。

常见的简单实现

1.使用 static 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class S1 {
public:
static S1& GetInstance() {
static S1 s;
return s;
}
void fun() {}

private:
S1(){cout<<"S1 inited"<<endl;}
S1(const S1&) = delete;
S1& operator=(const S1&) = delete;
};

class S2{
public:
static S2& GetInstance(){
return s;
}
void fun(){}
private:
S2(){cout<<"S2 inited"<<endl;}
//...
private:
static S2 s;
};
S2 S2::s;

其中 S2 是要避免的。因为 1. 类的静态成员变量的初始化时间一般早于 main 函数 2. 如果静态成员的初始化里调用了其它类,可能出现未定义的错误。

2.使用指针判断是否初始化

1
2
3
4
5
6
7
8
9
10
11
12
class S3{
public:
static S3& GetInstance(){
if(s == 0)
s = new S3();
return *s;
}

private:
S3(){}
static S3 *s;
};

这两种方式在单线程程序中使用都是可以的,但如果在多线程中,就会出现问题。

Magic Static

对于 S1 要注意, C++ 局部静态变量的初始化可能不是线程安全的 , 这就是 Magic Static ,是指 返回一个静态局部变量的引用 的用法,在某些情况下,如S1 可能会被编译器这样解析:

1
2
3
4
5
6
7
8
9
10
//伪代码
static S1& GetInstance(){
static bool constructed = false;
static uninited S1 s;
if(!constructed){
constructed = true;
new (&s)S1;
}
return s;
}

这不是线程安全的。这里的 “某些情况” 是指 C++11之前的编译器。尽管C++11 标准规定这个过程应该是线程安全的,但是在 VS2015 之前,微软还是未实现这个标准。参见这里
Thread-Safe “Magic” Statics Static local variables are now initialized in a thread-safe way, eliminating the need for manual synchronization. Only initialization is thread-safe, use of static local variables by multiple threads must still be manually synchronized. The thread-safe statics feature can be disabled by using the /Zc:threadSafeInit- flag to avoid taking a dependency on the CRT. (C++11)
以及这里这里

Double-Checked Locking Problem

对于 S3, 在多线程中, “s==0” 这个条件可能会被多个线程判断为 true, 这会导致 s 被多次初始化, 而只有最后一次初始化有效, 那么对其改进如下:

1
2
3
4
5
6
static S3& GetInstance(){
Lock lock;
if(s == 0)
s = new S3();
return *s;
}

这是线程安全的,但引出的问题是,每次都会调用该方法都要加解一次锁,加解锁的过程是很耗资源的。那么对于上面的代码可以有如下改进:

1
2
3
4
5
6
7
8
static S3& GetInstance(){
if(s == 0){
Lock lock;
if(s == 0)
s = new S3();
}
return *s;
}

这段代码解决了每次调用都要加解锁问题。但它仍然不是线程安全的。编译器可能会将代码解析成这样:

1
2
3
4
5
6
7
8
9
10
11
static S3& GetInstance(){
if(s == 0){
Lock lock;
if(s == 0){
S3* tmp = operator new(sizeof(S3));
tmp = new (s) S3;
s = tmp;
}
}
return *s;
}

但也可能解析成这样:

1
2
3
4
5
6
7
8
9
10
static S3& GetInstance(){
if(s == 0){
Lock lock;
if(s == 0){
s = operator new(sizeof(S3));
new (s) S3;
}
}
return *s;
}

对于后一种情况,它将先为 s 分配一段内存,再将为它构建对象: 它不是线程安全的。

如图,当线程1为 s分配内存后,线程2 判断 s==0 为flase, 此时线程2 将返回一个已分配内存但未初始化的指针, 可能会引起程序崩溃。

volatile

在网上有很多帖子认为使用 volatile 可以解决 DCLP , 实际情况是, 使用 volatile 无法完美解决这个问题。这个我们下一节再作讨论。

基于 C++11 的解决方案

C++11 为我们提供了很多方便好用的同步原语,可以帮助我们解决前面提到的问题。

atomic

将变量声明为原子的,可以确保双检锁正确执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class S3 {
public:
static S3& GetInstance() {
if (s == 0) {
std::lock_guard<std::mutex> locker(mu);
if (s == 0) {
s = new S3();
}
}
return *s;
}

void fun() { cout << "aa" << endl;
}

private:
S3(){};
static std::mutex mu;
static std::atomic<S3*> s;
};

std::mutex S3::mu;
std::atomic<S3*> S3::s = 0;

callonce

使用 callonce /onceflag 则是一种更优雅的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class S3 {
public:
static S3& GetInstance() {
std::call_once(flag_, []() { s = new S3(); });
return *s;
}

void fun() {}

private:
S3() = default;

private:
static S3* s;
static std::once_flag flag_;
};

S3* S3::s;
std::once_flag S3::flag_;

使用模板的单例模式

更多的时候, 我们是仅定义一个单例模式的基类, 使需要使用单例的类来继承这个基类。定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template <class T>
class SingletonBase {
public:
static T* GetInstance() {
std::call_once(flag_, [&]() { ins_.reset(new T); });
return ins_.get();
}

virtual ~SingletonBase<T>() = default;

protected:
SingletonBase<T>() = default;
SingletonBase<T>(const SingletonBase<T>&) = delete;
SingletonBase<T>& operator=(const SingletonBase<T>&) = delete;

protected:
static std::unique_ptr<T> ins_;
static std::once_flag flag_;
};

template <class T>
std::unique_ptr<T> SingletonBase<T>::ins_;

template <class T>
std::once_flag SingletonBase<T>::flag_;

class S3 : public SingletonBase<S3> {
friend class SingletonBase<S3>;

public:
void fun() {}
~S3() = default;

private:
S3() {}
};

多参数构造函数的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
template <class T>
class SingletonBase {
public:
template <class... Args>
static T* GetInstance(Args&&... args) {
std::call_once(flag_, [&]() { ins_.reset(new T(std::forward<Args...>((args)...))); });
return ins_.get();
}

static T* GetInstance() {
if (!ins_) throw std::logic_error("please init class first");
return ins_.get();
}

virtual ~SingletonBase<T>() = default;

protected:
SingletonBase<T>() = default;
SingletonBase<T>(const SingletonBase<T>&) = delete;
SingletonBase<T>& operator=(const SingletonBase<T>&) = delete;

protected:
static std::unique_ptr<T> ins_;
static std::once_flag flag_;
};

template <class T>
std::unique_ptr<T> SingletonBase<T>::ins_;

template <class T>
std::once_flag SingletonBase<T>::flag_;

//BAD inherit
class S3 : public SingletonBase<S3> {
friend class SingletonBase<S3>;

public:
void fun() {}
~S3() = default;

private:
S3() {}
};

class S4 : public SingletonBase<S4> {
friend class SingletonBase<S4>;
public:
void fun() {}

private:
S4(const int&& n) : size_(n) {}

private:
int size_;
};

int main() {
S3::GetInstance()->fun(); //BAD inherit
S4::GetInstance(1)->fun();
S4::GetInstance()->fun();
return 0;
}

此时必须注意,需要和无参数的单例模式分开来。