Singleton模式概念:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
然而真正实现起来却并非易事,甚至有些棘手。棘手之处在于删除问题,如何删除Singleton实例?谁来删除?何时删除?
在[DP]中,并未探讨有关析构和删除的问题,这是GoF的疏忽。此模式虽小,内涵却多,随着观察的深入,问题便突显出来。之后,John Vlissides(GoF之一)在Pattern hatching(1998)一书中探讨了这个问题。
和工厂模式等不同,Singleton对象需要自己对「所有权」进行管理,用户无权删除实例。
Singleton对象从创建到删除之间便是其生命期,然而,我们只知道创建时间,而不知其删除时间,也就无法管理所有权。
事实上就算我们不进行释放操作,程序在结束之时,操作系统也会将进程所用的内存完全释放,并不会产生内存泄漏。然而,若Singleton产生的实例在构造时申请过广泛的资源(通常是内核资源,如套接字,信号量,互斥体,事件),便会产生资源泄漏。解决办法就是在程序关闭时正确地删除Singleton对象,此时,删除时机便至关重要。
Singleton模式还有一个问题是多线程支持,不过C++11之后并非主要问题。
总结一下,Singleton模式虽然易于表达和理解,但却难以实现。关键在于三点:创建,删除,多线程。其中删除是最棘手的问题,能同时满足这三点的实现,便能适应几乎所有情况。
1 Singleton的唯一性
Singleton模式是一种经过改进的全局变量,重点集中于“产生和管理一个独立对象,并且不允许产生另外一个这样的对象”。
也就是说具有唯一性,因此一切能够再次产生同样对象的方式都应该被杜绝,比如:构造函数,拷贝构造,移动构造,赋值操作符。
[DP]中的定义如下:
1class Singleton
2{
3public:
4
5 static Singleton* Instance() {
6 if(!pInstance_) {
7 pInstance_ = new Singleton;
8 }
9 return pInstance_;
10 }
11
12private:
13 Singleton();
14 Singleton(const Singleton&) = delete;
15 Singleton& operator=(const Singleton&) = delete;
16 Singleton(Singleton&&) = delete;
17 Singleton& operator=(Singleton&&) = delete;
18 ~Singleton();
19
20 static Singleton* pInstance_;
21};
22Singleton* Singleton::pInstance_ = 0;
这种方式的确满足了唯一性,用户除了从Instance()获取对象之外,别无他法。且用户无法意外删除对象,因为析构函数也被私有了(不过依然可以由返回的指针删除之,所以最好以引用返回)。
最大的问题在于此法只满足三大关键之一:创建,对于删除和多线程都不满足。
所以,在满足唯一性的前提下,Singleton的实现还应设法管理产生对象的生命期和多线程环境下的访问安全。
2 隐式析构的Singleton
1节的Singleton只有创建,没有删除,这可能会导致资源泄漏,一种解决方法是使用隐式析构。
C++的static成员变量的生命期伴随着整个程序,在程序关闭时由编译器负责销毁。于是我们可以创建一个static嵌套类,在其析构函数中释放singleton对象。
具体的实现如下:
1class singleton
2{
3public:
4 static singleton& instance()
5 {
6 if(!pInstance_)
7 {
8 destroy_.create();
9 std::cout << "create\n";
10 pInstance_ = new singleton;
11 }
12 return *pInstance_;
13 }
14
15private:
16 static singleton* pInstance_;
17
18 // embedded class
19 // implicit deconstructor
20 struct singleton_destroyer {
21 ~singleton_destroyer() {
22 if(pInstance_) {
23 std::cout << "singleton destroyed!\n";
24 delete pInstance_;
25 }
26 }
27 void create() {}
28 };
29 static singleton_destroyer destroy_;
30
31protected:
32 singleton() = default;
33 virtual ~singleton() {}
34 Singleton(const Singleton&) = delete;
35 Singleton& operator=(const Singleton&) = delete;
36 Singleton(Singleton&&) = delete;
37 Singleton& operator=(Singleton&&) = delete;
38};
39
40singleton* singleton::pInstance_ = nullptr;
41singleton::singleton_destroyer singleton::destroy_;
此时singleton将能够具有摧毁功能,此时再加入线程处理也并非难事,如何保证线程安全可以参见7.5节。
3 Meyers singleton
隐式析构的Singleton的确是一种不错的实现方式,然而C++实现Singleton最简单而有效的方法还是Meyers singleton,实现如下:
1class singleton
2{
3public:
4 static singleton& instance() {
5 static singleton obj;
6 return obj;
7 }
8
9private:
10 Singleton();
11 Singleton(const Singleton&) = delete;
12 Singleton& operator=(const Singleton&) = delete;
13 Singleton(Singleton&&) = delete;
14 Singleton& operator=(Singleton&&) = delete;
15 ~Singleton();
16};
此法并未使用动态分配和静态指针,而是借用了一个局部静态变量。
和上一个方法一样,该法也是到第一次执行时才会初始化对象(网上有些地方称之为懒汉式,然而在正统C++设计模式相关书籍中,皆未出现过此种叫法),它返回的是引用,所以用户无法对返回的对象进行delete操作(暴力转换法不算:))。此外,在C++11之后,这种方法也是线程安全的,所以对于大多情况下,这是最简单也是最有效的实作法,仅有两行代码。
** Meyers singleton 关键之处是让static对象定义在函数内部,变成局部static变量**
如果是把 static对象定义成 Singleton的私有static成员变量,然后getInstance()去返回这个成员即:
class Singleton {
public:
static Singleton& getInstance() {
return inst;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 其他数据函数
// ...
private:
Singleton() { ... }
static Singleton inst;
// 其他数据成员
// ...
};
Singleton Singleton::inst;
虽然它也是 先getInstance()再访问,但这种不是Meyers’ Singleton!
那么为什么Meyers推荐的是第一种的呢?
原因是这解决了一类重要问题,那就是static变量的初始化顺序的问题。
C++只能保证在同一个文件中声明的static变量的初始化顺序与其变量声明的顺序一致。但是不能保证不同的文件中的static变量的初始化顺序。
然后对于单例模式而言,不同的单例对象之间进行调用也是常见的场景。比如我有一个单例,存储了程序启动时加载的配置文件的内容。另外有一个单例,掌管着一个全局唯一的日志管理器。在日志管理初始化的时候,要通过配置文件的单例对象来获取到某个配置项,实现日志打印。
这时候两个单例在不同文件中各自实现,很有可能在日志管理器的单例使用配置文件单例的时候,配置文件的单例对象是没有被初始化的。这个未初始化可能产生的风险指的是C++变量的未初始化,而不是说配置文件未加载的之类业务逻辑上的未初始化导致的问题。
而Meyers’ Singleton写法中,单例对象是第一次访问的时候(也就是第一次调用getInstance()函数的时候)才初始化的,但也是恰恰因为如此,因而能保证如果没有初始化,在该函数调用的时候,是能完成初始化的。所以先getInstance()再访问 这种形式的单例 其关键并不是在于这个形式。而是在于其内容,局部static变量能保证通过函数来获取static变量的时候,该函数返回的对象是肯定完成了初始化的!
编译器会负责管理局部静态变量,当程序结束时进行析构,看样子一切安好。然而,当生成的多个Singleton对象具有依赖关系时,Meyers singleton就无能为力了(隐式析构Singleton亦如此)。
假设我们有四个Singleton,Director(导演)、Scene(场景)、Layer(图层)、Log(日志)。我们处于一个简单的游戏中,该游戏只有一个导演,一个场景,一个图层,图层上会有若干精灵。我们依次创建,到Layer时初始化失败了,此时创建一个Log来记录崩溃原因。之后程序关闭,执行期的相关机制会来摧毁静态对象,摧毁的顺序是LIFO,所以会先摧毁Log,然后是Layer。但若是Scene关闭失败,此时再想向Log记录崩溃原因,由于Log已被摧毁,所以返回的用只是一个“空壳”,之后的操作便会发生不确定性行为,这种问题称为dead-reference。
dead-reference问题的原因在于C++并不保证静态对象析构的顺序,因此具有依赖关系的多个Singleton无法正确删除。
使用局部静态变量的另一个缺点是难以通过派生子类来扩展Singleton,因为instance创建的始终都是singleton类型的对象。不过可以通过泛型编程来解决,见5节。
4 可以复活的Singleton
要满足具有依赖关系的Singleton,其中一个思路是当需要再次用到已经被执行期处理机制删除的Singleton对象时,使其死灰复燃,复活的Singleton需要自己负责删除。
所以首先需要增加一个标志来记录singleton是否已被摧毁,若已摧毁,则使其复生;若未摧毁,则创建Singleton对象。
大体上的代码如下:
1class singleton
2{
3public:
4 static singleton& instance()
5 {
6 if(!pInstance_)
7 {
8 if(destroyed_)
9 on_dead_reference(); // 出现dead-reference,重生singleton
10 else
11 create(); // 创建singleton
12 }
13 return *pInstance_;
14 }
15private:
16 static singleton *pInstance_;
17 static bool destroyed_; // 记录是否已被摧毁
18};
那么具体就是要看on_dead_reference和create这两个函数是如何处理的,create和之前一样,可以直接使用Meyers singleton:
1static void create()
2{
3 static singleton obj;
4 pInstance = &obj;
5}
当singleton对象析构时,也就是摧毁实例时,改变destroyed_标记(默认为false),将其改为摧毁状态:
1virtual ~singleton()
2{
3 pInstance = nullptr;
4 destroyed_ = true;
5}
而如何让对象死灰复燃呢?我们可以借助placement new,在对象死去的“空壳”上重新创建对象。
1void on_dead_reference()
2{
3 create();
4 new(pInstance_) singleton;
5 std::atexit(kill_singleton);
6 destroyed_ = false;
7}
这里的关键在于atexit函数,该函数可以注册一些函数,注册的函数将会在程序结束之时被调用。事实上static成员变量的默认释放也是通过此函数完成的,编译器会自动生成释放函数向atexit注册,释放机制是LIFO。
此时我们是通过placement new重新构造了singleton对象,所以需要自己释放,因此定义一个kill_singleton函数:
1static void kill_singleton()
2{
3 pInstance_->~singleton();
4}
只需调用析构函数释放便可。
reference: https://mp.weixin.qq.com/s/ufDK34vWC6yTSkS3TmucWw
https://www.zhihu.com/question/56527586
https://github.com/lkimuk/okdp/blob/master/singleton/singleton.hpp