从单例模式谈起(一)
目录
单例模式可能是大家最为熟知的一种设计模式,本身没什么好谈的。但是在 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 63 |
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; } |
此时必须注意,需要和无参数的单例模式分开来。