单例模式是使用最普遍,也最简单的设计模式之一,做用是保证一个类只有一个实例。单例模式是对全局变量的一种改进,避免全局变量污染命名空间。由于如下几个缘由,全局变量不能做为单例的实现方式:c++
1. 不能保证只有一个全局变量编程
2. 静态初始化时可能没有足够的信息建立对象设计模式
3. c++中全局对象的构造顺序是未定义的,若是单件之间存在依赖将可能产生错误安全
单例模式的实现代码很简单:多线程
//singleton.hpp #ifndef SINGLETON_HPP #define SINGLETON_HPP class Singleton{ public: static Singleton* getInstance(); private: static Singleton* pInstance; }; #endif 1 //singleton.cpp 2 #include "singleton.hpp" 3 4 Singleton* Singleton:: pInstance = nullptr; 5 6 Singleton* Singleton::getInstance(){ 7 if(nullptr == pInstance){ 8 pInstance = new Singleton; 9 } 10 return pInstance; 11 }
单例模式这么简单,原本讲到这里就能够结束了。不过若是把上面代码放到多线程编程中使用就不那么可靠了。在《C++and the Perils of Double-Checked Locking》这篇文章中,Scott Meyers和Andrei Alexandrescu以单例模式为例详细讲述了多线程编程中的坑。下面的内容基本出自这篇论文,跟你们分享一下,很是经典。函数
上面的实如今单线程时没有问题,如今假设有两个线程A和B,A执行到第8行后因中断挂起,这时候instance尚未建立,B执行到第8行,因而A和B都会建立Singleton对象,性能
如今就有两个单例对象了,这固然是错误的。改为线程安全很不难,进入 getInstance加个锁就能保证每次只有一个线程进入函数,因而只会有一个线程实例化 pInstance。优化
Singleton* Singleton::getInstance(){ Lock lock; if(nullptr == instance){ pInstance = new Singleton; } return pInstance; }
可是每次调用 getInstance都加锁是一件效率很是低的事情,特别是这里只有第一次实例化 pInstance 时才须要互斥,之后都不须要锁。因而DCLP(Double-Checked Locking Pattern)产生了。ui
DCLP的经典实现以下:atom
Singleton* Singleton::instance() { if (pInstance == 0) { // 1st test Lock lock; if (pInstance == 0) { // 2nd test pInstance = new Singleton; } } return pInstance; }
经过两次检测 pInstance,这样实例化后全部的调用都不须要加锁。看样子问题已经解决了,互斥锁保证了只有一个线程会实例化 pInstance,之后的调用不须要锁,性能也不会有问题,很完美是否是。让咱们一步步来看看这里面隐藏的坑。
pInstance = new Singleton;
这条实例化语句其实作了3件事情:
1. 分配一块动态内存
2. 在这块内存上调用Singleton构造函数构造对象
3. pInstance指向这块内存
问题的关键是第2和第3步可能会被编译器因优化缘由调换顺序,先给pInstance赋值,在构建对象。在单线程上这是行的通的,由于编译器优化的原则是不改变结果,调换2,3两步对结果并无影响。因而代码就相似于下面这样:
Singleton* Singleton::instance() { if (pInstance == 0) { Lock lock; if (pInstance == 0) { pInstance = // Step 3 operator new(sizeof(Singleton)); // Step 1 new (pInstance) Singleton; // Step 2 } } return pInstance; }
再来考虑两个线程A和B,
1. A第一次检查 pInstance,获取锁,执行第1和第3步,挂起,这时候 pInstance非空,可是尚未调用构造函数,pInstance指向的是未初始化内存。
2. 线程B检查 pInstance,发现非空,因而跳出函数,后面开始使用 pInstance,一个未初始化的对象。
DCLP只有在步骤1,2,3按照严格顺序执行时才能保证正确,然而,c/c++并无这方面的支持,c/c++语言自己没有多线程,编译器优化只要保证单线程语义正确就行,多线程是不考虑的。为了保证第2步在第3步以前完成,可能须要增长一个临时变量,
Singleton* Singleton::instance() { if (pInstance == 0) { Lock lock; if (pInstance == 0) { Singleton* temp = new Singleton; // initialize to temp pInstance = temp; // assign temp to pInstance } } return pInstance; }
很惋惜,temp极可能也会被编译器优化掉。为了防止优化,文章围绕volatile关键字作了详细的讨论,刘未鹏以及何登成都深刻解释了volatile关键字在多线程编程中的效果,volatile明确告诉编译器不要对被修饰的变量作优化,包括读写值时必须直接读取内存值,两个volatile变量的前后顺序不可变等。不过
1. volatile只能保证单线程内指令顺序不变,不能保证多线程间的指令顺序的正确性
2. 一个volatile对象只有在构造函数完成后才具备volatile特性,因此仍然存在前面讨论的问题。
总之,volatile没法保证多线程正确。
另外,在多处理器机器上,还存在cache一致性问题。若是线程A和B在不一样的处理器上,
即便A严格按照1,2,3步骤执行,在将cache写回主存的过程当中仍然可能改变顺序,由于按照内存地址升序顺序写回数据能够提升效率。
完全的解决方法是使用memory barrier,这篇文章给出了c++11中的作法,
std::atomic<Singleton*> Singleton::instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance(){ Singleton* tmp = instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); if(nullptr == tmp){ std::lock_guard<std::mutex> lock(m_mutex); tmp = instance.load(std::memory_order_relaxed); if(nullptr == tmp){ tmp = new Singleton(); std::atomic_thread_fence(std::memory_order_release); instance.store(tmp, std::memory_order_relaxed); } } return instance; }
为了实现线程安全的DCLP,可谓费劲周章。其实有时候咱们也能够采起另外的解决问题的方式,好比多线程程序开始只有主线程,咱们能够先在主线程中初始化单例模式,而后再建立其余线程,从而彻底避免以上问题,这也是咱们公司项目中采用的方法!
Reference
http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/
C++ and the Perils of Double-Checked Locking