Home Page
Search
\begin{md} **转载** [C++单例模式](https://blog.csdn.net/qq_35280514/article/details/70211845) ## 单例模式 在一些情形下,保持类的实例只有一个非常重要。例如:一个表示文件系统的 Class。一个操作系统一定是只有一个文件系统的,因此,我们希望表示文件系统的类实例有且仅有一个。**单例模式** 是设计模式中一种实现这一类需求的设计方法。 **单例模式(Singleton)**,保证一个类仅有一个实例,并提供一个访问它的全局访问点 全局静态变量能够实现对象的全局访问,但这不能防止你实例化多个类实例。为了实现上述要求,我们需要加强类的设计,让类自身保证其实例仅有一个。也就是说,“这个类可以保证没有其它实例可以被创建,并且它可以提供一个访问该实例的方法。” 一般来说,单例类的结构如下 ```c++ class Singleton { public: static Singleton* GetInstance(); // 供用户获取单例的全局访问点 protected: Singleton(); // 方便继承。同时保证类的用户无法直接构造该类的实例 Singleton(const Singleton&); private: // other class member }; ``` ## C++ 中 static 对象的初始化 C++规定,**non-local static 对象的初始化发生在 main 函数执行之前**。但 C++没有规定多个 non-local static 对象的初始化顺序,尤其是来自多个编译单元的 non-local static 对象,他们的初始化顺序是随机的。 然而,**对于 local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。** non-local static 对象的初始化发生在 main 函数之前的单线程启动阶段,所以无需担心线程安全问题。但是 local static 对象则不同,多个线程的控制流可能同时到达其初始化语句。 **在 C++11 之前,在多线程环境下 local static 对象的初始化并不是线程安全的**。[[2]](http://stackoverflow.com/a/1661564)具体表现就是:如果一个线程正在执行 local static 对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该 local static 对象的构造函数中。这会造成这个 local static 对象的重复构造,进而产生内存泄露问题。 在文章的后半部分会看到,local static 对象在单例模式中有着广泛的应用,为了解决 local static 对象在多线程环境下的重复构造问题,程序员想出了很多方法。而 C++11 则在语言的规范中解决了这个问题。**C++11 规定,在一个线程开始 local static 对象的初始化后完成初始化前,其他线程执行到这个 local static 对象的初始化语句就会等待,直到该 local static 对象初始化完成。** [2](http://stackoverflow.com/a/1661564) ## C++实现单例模式 单例模式的实现分为两大类,懒汉模式和饿汉模式。懒汉模式的单例秉承着实例能晚一点构造就晚一点构造的思想,直到第一次使用单例时才构造单例;饿汉模式则恰好相反,即使实例永远不会被使用,实例的构造还是会早早的发生。 ### 懒汉模式 懒汉模式有以下几点要求: * 保证类实例是唯一的 * 提供全局可访问点 * 延迟构造,直到第一次使用该实例 以下是几种懒汉模式的单例模式实现.[例子](http://www.klayge.org/?p = 3280)[例子](http://www.cnblogs.com/zxh1210603696/p/4157294.html) 他们或者利用了 local static 对象的特性,或者使用指针来判断对象是否是第一次初始化。在多线程条件下,则需要互斥锁以避免重复构造问题 。 首先是《设计模式》中给出的单例模式实现。 ```c++ template
T& Singleton() { static T instance; return instance; } ``` 这一实现在 C++98 中不是线程安全的,具体原因参考上一节关于 static 对象的初始化的讨论。 为了解决上一段代码中存在线程安全问题,最简单的方法是使用 **互斥锁** 在开始初始化之前先获取锁。 ```c++ std::mutex m; // 必须是一个全局变量 template
T& Singleton() { std::unique_lock lock(m); static T instance; return instance; } ``` 但是,线程安全问题只是出现在第一次初始化过程。然而,以上代码却为了一次初始化而使得每一次获取 Singleton 都要首先获取锁资源,Singleton 的访问变成了串行。这显然不太合算。 为了避免每一次都加锁我们可以事先判断是否已经初始化。**Double Check Lock** 是一种常用的实现手法。如下: ```c++ class Singleton { private: Singleton(); Singleton(const Singleton &); public: static Singleton& GetInstance() { if (m_instance == nullptr) { std::unique_lock lock(m); if (m_instance == nullptr) { m_instance = new Singleton; } } return *m_instance; } private: static Singleton *m_instance = nullptr; static std::mutex m; }; ``` 如果内存访问严格按照语句先后顺序进行,那么以上代码堪称完美解决了所有问题。但是,在某些内存模型中(虽然不常见)或者是由于编译器的优化以及运行时优化等等原因,使得 m_instance 虽然已经不是 nullptr 但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用 GetInstance()就有可能使用到一个不完全初始化的对象。(double check 中的互斥锁在此处是不起作用的) [内存屏障](http://baike.baidu.com/item/内存屏障?sefr = enterbtn)可以强制要求内存完成屏障前的所有内存读写操作。C++11 提供了 Atomic 实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。[参考资料 3](http://www.klayge.org/?p=3280) 中给出了多个实现内存同步访问的示例。以下是利用 atomic 实现的版本: ```c++ class Singleton { private: Singleton(); Singleton(const Singleton&); public: static Singleton& GetInstance() { if (m_instance == nullptr) { std::unique_lock lock(m); if (m_instance == nullptr) { m_instance = new Singleton; } } return *m_instance; } private: static std::atomic
m_instance(nullptr); static std::mutex m; }; ``` 综合以上种种我们发现,迫使我们做各种繁琐工作的罪恶来源是 C++98 中没有规定 local static 对象在多线程条件下的初始化行为。然而在上一节的讨论中我们知道 C++11 给出了规定。因此,在 C++11 标准下一个线程安全的单例模式可以这样实现。这种方法也被称为 **Meyers’ Singleton**: ```c++ class Singleton { private: Singleton(); SIngleton(const Singleton&); public: static Singleton& GetInstance() { static Singleton instance; return instance; } private: // other class member }; ``` 是不是有一种大道至简的感觉?这和我们最开始给出的形式一模一样。但是注意,此时的语言标准变了,在 C++11 标准下这样写才是线程安全的。另外 GCC4.3 和 VS2015 之后的版本也开始支持该特性。 ### 饿汉模式 饿汉模式下同样要求单例是类的唯一实例,且需要类提供一个全局访问点。但是与懒汉模式不同,饿汉模式中单例的构造会尽可能早的进行,即使目前不会用到,甚至以后永远也不会用到。 以下是几种饿汉模式的单例模式实现。从上一节的讨论中我们可以知道 non-local static 对象符合饿汉模式对于单例构造应该尽早进行的要求。然而,一旦使用了 non-local static 对象我们就不得不小心多个 non-local static 对象的初始化问题。 ```c++ class Singleton { private: Singleton(); Singleton(const Singleton&); struct InstanceCreator { InstanceCreator() { Singleton::m_instance = new Singleton; } ~InstanceCreator() { delete Singleton::m_instance; Singleton::m_instance = nullptr; } }; public: static Singleton& GetInstance() { return *m_instance; } private: friend class InstanceCreator; static Singleton *m_instance; static InstanceCreator m_creator; }; ``` 如果没有其他 non-local static 对象使用这个单例,那么上述代码是可以正常使用的。然而,在有其他 non-local static 对象使用该单例时,上述代码就会出现问题。由于 non-local static 对象初始顺序的不确定性,我们无法让具有依赖关系的两个 non-local static 对象按照依赖关系的固定顺序初始化。 事实上,在绝大多数条件下,类的设计阶段我们无法得知会不会有一个 non-local static 对象依赖于这个单例,即使可以确定目前没有其他 non-local static 对象依赖于这个单例,那也无法确定在以后也不会有。所以我们需要设计一个方案来应对可能出现的两种情况。以下代码可以满足这种需求。 ```c++ class Singleton { private: Singleton(); Singleton(const Singleton&); struct InstanceCreator { // 真正负责单例的构造和析构 InstanceCreator() { Singleton::m_instance = new Singleton; } ~InstanceCreator() { delete Singleton::m_instance; Singleton::m_instance = nullptr; } }; struct InstanceConfirm { // //没有non-loacl static对象依赖时,保证在main函数前初始化 InstanceConfirm() { Singleton::GetInstance(); } } public: static Singleton& GetInstance() { //全局访问点 static InstanceCreator creator; return *m_instance; } private: friend class InstanceCreator; friend class InstanceConfirm; static Singleton *m_instance; static InstanceConfirm m_confirm; // //最后保证main函数之前完成初始化 }; ``` 以上代码在 C++98 标准下也可以正常工作,唯一的要求是在 main 函数之前进程是单线程的。 ## C++单例模式的实现总结 在 C++单例模式的多种实现中,**Meyers’ Singleton 是绝对的第一选择!** 但要注意的是,其多线程安全性在 C++11 的标准下才能得以保证(或者编译器特性支持)。 如果由于各种原因(包括为兼容旧代码等等)Meyers’ Singleton 无法在多线程条件下工作。那么,个人认为也没有必要为了强行实现懒汉模式而搞一大堆的同步代码,直接实现一个饿汉模式的单例即可。这一过程应该注意多个 non-local static 对象的初始化次序问题 [6]。 \end{md}
Home Page