单例模式 | C & C++

单例模式是使用最广泛的设计模式之一,其目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点。

Eager Singleton

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
public:
static Singleton& GetInstance() {
return instance;
}
private:
Singleton() {};
~Singleton() {};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);

static Singleton instance;
};

由于在 main 函数之前初始化,所以该实现方式没有线程安全的问题。但是潜在问题在于 no-local static 对象(函数外的 static 对象)在不同编译单元中的初始化顺序是未定义的。也即 static Singleton instance;static Singleton& GetInstance() 二者的初始化顺序不确定,如果在初始化完成之前调用 GetInstance() 方法会返回一个未定义的实例。

Lazy Singleton

线程不安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
public:
static Singleton* GetInstance() {
if (instance == nullptr)
instance = new Singleton();
return instance;
}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);

static Singleton* instance;
};

注:

  • C++ 规定 const 静态类成员可以直接初始化,其他非 const 的静态类成员需要在类声明以外初始化,我们一般选择在类的实现文件中初始化。
  • 静态成员在 cpp 文件中也声明一下,否则编译时会提示 undefined reference。

线程安全,存在 memory order 潜在问题:双检测锁模式(Double-Checked Locking Pattern, DCLP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Singleton {
public:
static Singleton* GetInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> mutx(some_mutex);
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);

static Singleton* instance;
std::mutex some_mutex;
};

如果第一次检测不做则会每次获取都加锁,在实例已经创建的情况下这是没有必要的。在第一次检测实例是否被创建和加锁的操作之间,可能有另一个线程创建了实例,所以第二次检测也是必不可少的。

线程安全:atomic

DCLP 其实也存在问题。在某些内存模型中或者是由于编译器的优化以及运行时优化等等原因,使得 instance 虽然已经不是 nullptr 但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用 GetInstance() 就有可能使用到一个不完全初始化的对象。在 C++11 没有出来的时候,只能靠插入两个 memory barrier(内存屏障)来解决这个错误,但是 C++11 引进了 memory model,提供了 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 Singleton {
public:
static Singleton* GetInstance() {
Singleton* tmp = instance;
if (tmp == nullptr) {
std::lock_guard<std::mutex> mutx(some_mutex);
tmp = instance; // tmp = instance.load(memory_order_seq_cst);
if (instance == nullptr) {
tmp = new Singleton();
instance = tmp; // instance.store(tmp, memory_order_seq_cst);
}
}
return instance;
}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);

static std::atomic<Singleton*> instance;
std::mutex some_mutex;
};

最佳实践:Meyers’ Singleton

实现方式

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

单例模式的两个特性以如下方式被保证:

  • 线程安全性:C++11 规定了局部静态变量在多线程条件下的初始化行为,要求编译器保证了局部静态变量的线程安全性
  • 单次初始化:C++11 规定了局部静态变量在代码第一次执行到变量声明的地方时初始化,局部静态变量的特性保证了其唯一性

另外,C++11 还提供了 std::call_once 函数,也可以用来实现多线程安全的单例对象初始化。

可能存在的问题

在这样一种情况下:

  • 有一个动态库和一个调用该动态库的主程序
  • Meyers Singleton 类定义在动态库中
  • 在动态库的 .cpp 文件和主程序中分别调用 GetInstance() 方法

这两次调用的单例其实并不是同一个对象。

造成这种现象的原因是每个动态库都有自己的静态数据实例。解决方法主要有:

  • 避免在库中调用单例
  • 使用 thread locale storage
  • 构建方式换成静态库

参考

C++ 单例模式

c++ - Singleton instanced two times

作者

zhongtian

发布于

2020-08-07

更新于

2023-12-16

许可协议

评论