用单例模式来讲解c++11新特性
单例模式定义
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
——《设计模式》
代码一
#include <stdlib.h>
using namespace std;
class Singleton {
public:
static Singleton* GetInstance(){
if(_instance == nullptr){
_instance = new Singleton();
}
return _instance;
}
private:
Singleton(){}; //普通构造
~Singleton(){};
Singleton(const Singleton &) = delete; //拷贝构造
Singleton& operator=(const Singleton &) = delete; //拷贝赋值构造
Singleton(Singleton &&) = delete; //移动构造
Singleton& operator=(Singleton &&) = delete; //移动拷贝构造
static Singleton *_instance;
}
Singleton* Singleton::_instance = nullptr;
代码一说明
在单例模式中,为了确保一个类对应一个实例,不仅要考虑只能创建一个实例,而且要考虑创建一个实例后,这个实例又被调用去创建另一个实例。所以,要将普通构造和析构函数设置为私有,然后禁用四类特殊构造函数。在这里就使用了c++11的新特性:Singleton(const Singleton &) = delete,可以实现将某个构造函数禁用的功能。
代码一的问题
没有安全地释放内存,而且私有的析构函数不能在外部调用,无法灵活地释放动态内存,容易造成内存泄漏,所以需要加一个静态的释放内存的接口。
代码二
#include <stdlib.h>
using namespace std;
class Singleton {
public:
static Singleton* GetInstance(){
if(_instance == nullptr){
_instance = new Singleton();
atexit(Destructor);
}
return _instance;
}
private:
static void Destructor(){
if(nullptr != _instance){
delete _instance;
_instance = nullptr;
}
}
Singleton(){}; //普通构造
~Singleton(){};
Singleton(const Singleton &) = delete; //拷贝构造
Singleton& operator=(const Singleton &) = delete; //拷贝赋值构造
Singleton(Singleton &&) = delete; //移动构造
Singleton& operator=(Singleton &&) = delete; //移动拷贝构造
static Singleton *_instance;
}
Singleton* Singleton::_instance = nullptr;
代码二的说明
代码二增加了一个Destructor接口,然后使用atexit确保在程序结束时进行适当的清理工作。
代码二的问题
代码二只是atexit函数本身是线程安全的,但是GetInstance函数并不是线程安全的。例如,当多个线程刚好同时调用GetInstance函数,那么导致的结果是拥有多个全局实例,违背了单例模式的定义。这时候我们自然想到的是加锁。
代码三
#include <stdlib.h>
#include <mutex>
#include <iostream>
using namespace std;
class Singleton{
public:
static Singleton *GetInstance(){
if(_instance == nullptr){
//RAII 利用类的生命周期进行资源管理
lock_guard<mutex> lock(_mutex);
if(_instance == nullptr){
_instance = new Singleton();
// operator new
// 1. 分配内存
// 2. 调用构造函数
// 3.返回指针
// 多线程环境下 cpu reorder操作
atexit(Destructor);
}
}
return _instance;
}
private:
static void Destructor(){
if(nullptr != _instance){
delete _instance;
_instance = nullptr;
}
}
Singleton(){}; //构造
~Singleton(){};
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton& operator=(Singleton &&) = delete;
static Singleton *_instance;
static mutex _mutex;
};
Singleton *Singleton::_instance = nullptr;
mutex Singleton::_mutex;
代码三的说明
代码三中使用了线程锁lock_guard lock(_mutex),并且使用了双重检测技术(double check),来优化锁的调用。因为锁的调用,只在刚开始创建实例的时候使用一次,在使用锁之前再加一次判断,可在后来再次调用GetInstance时,直接判断,然后放回,而无需再加锁了。
注意的是,线程锁就是c++11的又一新特性,在本例中lock_guard lock(_mutex)可以在调用的声明周期结束的时候,自动解锁。
代码三的问题
代码三虽然优化了许多,但是这只适合跑单核的处理器,而现在普遍使用多核的处理器的环境下,系统会对程序进行优化,如:编译器重排、cpu重排,使用单核的思维去编程是会出现一些问题的,如:可见性问题、执行序问题。
例如上面例子中,上面的执行顺序本来是1->2->3的,后来被优化,变成了1->3->2,也就是先返回指针,在调用构造函数,为堆块空间赋初值。这样的顺序会造成不好的结果,因为当线程返回指针后,此时的_instance指针就非空了,然后其它线程就可以直接访问堆块空间。而此时的堆块空间可能还没有被构造,是一块空白区域。
代码四
#include <stdlib.h>
#include <mutex>
#include <atomic>
using namespace std;
class Singleton {
public:
static Singleton *GetInstance(){
Singleton* tmp = _instance.load(memory_order_acquire);//获取内存屏障
if(tmp == nullptr){
lock_guard<std::mutex> lock(_mutex);
tmp = _instance.load(memory_order_relaxed);
if(tmp == nullptr){
tmp = new Singleton;
_instance.store(tmp, memory_order_release);//释放内存屏障
atexit(Destructor);
}
}
return tmp;
}
private:
static void Destructor(){
Singleton* tmp = _instance.load(memory_order_relaxed);
if(nullptr != tmp){
delete tmp;
}
}
Singleton(){}; //构造
~Singleton(){};
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton& operator=(Singleton &&) = delete;
static Singleton *_instance;
static mutex _mutex;
};
std::atomic<Singleton*> Singleton::_instance;
std::mutex Singleton::_mutex;
代码四说明
代码四使用了一些c++11新特性来解决代码三的问题。其中引入了两个关键:原子变量 和 内存栏栅。
什么是原子变量呢?
他是c++11多线程中,使用无锁编程的基础。原子变量通过提供原子操作,确保对变量的读写操作是不可分割的,即在任何时刻都不会被中断或出现竞争条件。它在实现锁的功能的同时(即临界资源的读写问题),使多线程并发性能更加高效,代码也更加简洁。
什么是内存栏栅呢?
其也叫内存屏障,c++11中提供了6大内存模型
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
它是为了解决多线程中的执行序问题,说起来会很复杂,如果大家有详细了解的需求,点赞+关注,我会在下一期博客用更加小白的话给大家讲解。
好,回归上面例子。说白了就是在获取内存屏障和释放内存屏障之间的代码,就算被程序优化,也不会对其它线程造成影响,它执行的结果和没有被优化的结果相同。
注意:
Singleton* tmp = _instance.load(memory_order_acquire);//获取内存屏障
if(tmp == nullptr){
lock_guard<std::mutex> lock(_mutex);
tmp = _instance.load(std::memory_order_relaxed);
if(tmp == nullptr){
tmp = new Singleton;
_instance.store(tmp, memory_order_release);//释放内存屏障
上面代码中把原子变量的读取操作和内存栅栏融合在了一起,其等价于以下代码:
Singleton* tmp = _instance.load(memory_order_relaxed);
atomic_thread_fence(memory_order_acquire);//获取内存屏障
if(tmp == nullptr){
lock_guard<std::mutex> lock(_mutex);
tmp = _instance.load(memory_order_relaxed);
if(tmp == nullptr){
tmp = new Singleton;
atomic_thread_fence(memory_order_release);//释放内存屏障
_instance.store(tmp, memory_order_relaxed);
代码四的问题
为了写一个线程安全的程序,代码变得太长太复杂了,下面是一个大神提出的优化思路
代码五
// c++11 magic static 特性:如果变量在初始化的时候,并
//发同时进入声明语句,并发线程将会阻塞等待初始化结束
class Singleton{
public:
static Singleton& GetInstance(){
static Singleton instance;
return instance;
}
private:
Singleton(){}; //构造
~Singleton(){};
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton& operator=(Singleton &&) = delete;
}
/*
版本五具备所有优点:
1.利用静态局部变量特性,延迟加载;
2.利用静态局部变量特性,系统自动回收内存,自动调用析构函数
3.静态局部变量初始化时,没有new操作带来的cpu指令reorder操作
4.c++11 静态局部变量初始化时,具备线程安全
*/
代码五的说明
代码五利用了static的新特性,确保了多线程中,只能有一个线程可以初始化_instance,其它阻塞等待,然后返回值是_instance的引用,所以保持了唯一的实例。(总的来说,用得好不如用得巧)
代码五的问题
代码五的另一问题是代码的复用性差,要进一步改动
代码六
template<typename T>
class Singleton{
public:
static T& GetInstance(){
static T instance;
return T instance;
}
protected:
virtual ~Singleton(){}
Singleton(){} //protected修饰构造函数,才能让子类继承
private:
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton &&) = delete;
Singleton& operator=(Singleton &&) = delete;
};
class DesignPattern : public
Singleton<DesignPattern>{
friend class Singleton<DesignPattern>;
private:
DesignPattern(){}
~DesignPattern(){}
};
代码六说明
代码六就是引入了模板功能,这个就非常考验大家的c++的功底了。值得说明的是,在继承了Singleton后,还使用friend友元的原因是,为了在初始化_instance时,父类可以访问子类中的构造函数,因为子类的构造函数是私有的,这个情况就是“亦父亦友”的情形,比较经典。
更多推荐
所有评论(0)