《C++设计模式》视频_李建忠
大约 5 分钟
《C++设计模式》视频_李建忠
目录
[toc]
单件模式 Singleton
也叫单例模式(菜鸟教程中的叫法)
所属分类——“对象性能” 模式
- “对象性能” 模式
- 面向对象很好地解决了 “抽象" 的问题,但是必不可免地要付出一定的代价。 对于通常情况来讲,面向对象的成本大都可以忽略不计。 但是某些情况,面向对象所带来的成本必须谨慎处理。
- 典型模式
- 单件模式 Singleton
- 享元模式 Flyweight
- 补充:面向对象常见的性能代价
- 虚函数
动机(Motivation)
简概
- 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率
- 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
- 这应该是类设计者的责任,而不是使用者的责任
核心:保证一个类仅有一个实例,并提供一个该实例的全局访问点
代码体现
目的是让每一次都返回唯一的那个对象
单例类
class Singleton{
private: // 让外界不能使用以下的两个构造函数
Singleton() ; // 构造函数
singleton(const Singleton& other); // 复制构造函数
public:
static singleton* getInstance();
static Singleton* m_instance;
};
Singleton* Singleton::m_instance=nullptr;
版本1:线程非安全
- 存在问题
- 在多线程中,例如AB两个线程
- A执行完
if
(判断单例是否存在)还没执行下一行时,B恰好执行到if
那行 - 这就会产生两个单例
- 建议
- 单线程可以用
Singleton* Singleton::getInstance(){
if(m_instance == nullptr){
m_instance = new Singleton();
}
return m_instance;
}
版本2:线程安全 - 加锁版(Cleck Lock)
存在问题:
- 锁的代价过高
- 读的时候根本没必要去锁,性能浪费
建议
- 可以用,但高并发的话性能损耗大
Singleton* Singleton::getInstance(){
Lock lock;
if(m_instance == nullptr){
m_instance = new Singleton();
}
return m_instance;
}
版本3:线程安全 - 双检查锁(Double Cleck Lock)
存在问题
- 由于内存读写reorder不安全,会导致双检查锁失效
- reorder:从汇编指令的角度来看,
m_instance = new Singleton();
这行代码可以分解成三个步骤- (1) 分配内存
- (2) 调用构造器
- (3) 地址返回值给指针
- 但这三步只是理想指令执行顺序,实际情况中有可能 被reorder(重排顺序),顺序变成了 1-3-2
- 那么假设存在AB两个线程并发生以下情况
- 当A线程执行
m_instance = new Singleton();
时被reorder,即按1-3-2顺序执行指令 - 当A线程执行完指令3但还没执行指令2时
- B恰好依次执行代码第2行的判断、第8行的return。但此时构造器还未被调用,会出错
- 当A线程执行
建议
- 不要用,不安全,容易出错
Singleton* Singleton::getInstance(){
if(m_instance == nullptr){
Lock lock;
if(m_instance == nullptr){
m_instance = new Singleton();
}
}
return m_instance;
}
版本4:C++11之后的跨平台实现(volatile关键字)
- 简概
- C++11引入的新函数
- 功能
- 原理主要是让编译器不reorder
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
singleton* tmp = m_instance.load(std::memory_order_relaxed) ; // 不让用reorder
std::atomic_thread_fence(std::memory_order_acquire); // 获取内存fence
if(tmp==nullptr){
std::lock_guard<std: :mutex> lock(m_mutex); // 锁进程
tmp = m_instance.load(std::memory_order_relaxed); // 不让用reorder
if(tmp==nullptr){
tmp = new Singleton; // 此时不会出现reorder
std::atomic_thread_fence(std::memory_order_release); // 释放内存fence
m_instance.store(tmp, std::memory_order_relaxed); // 解除不让用reorder
}
}
return tmp;
}
版本5:C++11的 std::call_once()
简概
- C++11引入的新函数,感觉这个函数简直是为单例模式量身定做的
功能
- 能保证某个函数只被调用一次
- 能实现互斥量的功能,且效率上会更高
- 与if-else相比,在多线程中使用会更安全
原理:
- 第二个参数是需要调用的函数名
- 第一个参数是一个
std::once_flag
类型的标记。该标记将决定函数是否执行。 当执行过一次call_once()后,就会把这个标记设置为已调用状态。后续再调用时就会无法调用
使用:
using namespace std; std::once_flag g_flag; // 【标记】这是个标记 class Single { private: Single(){} static Single *m_instance; // 静态成员变量 static void CreateInstance() // 创建实例【只被调用一次】 { m_instance = new Single; } public: static Single * Instance() // 使用实例 { std:call_once(g_flage, CreateInstance); // 【call_once方法】传入标记、函数名作参 return m_instance; }; } Single *Single::m_instance = nullptr; // 初始化为空 // 线程入口函数 void mythread() { cout << "子线程" << endl; Single *single = Single::Instance(); // 使用单例对象 return; } int main { std::thread mytobj(mythread); return 0; }
版本6:直接使用static的简易版本
C++11中还有一种简单的单例,就是直接使用static。
C++11中可以保证static变量时多线程安全的,在底层实现了加锁操作,而且由于是static对象,也可以保证对象只生成一次
但用这种写法的基类可能因使用时操作不当不安全(比如不用static对象,自己又另外创建了一个)
设计模式
模式定义
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
——《设计模式》GoF
结构(Structure)
要点总结
- Singleton模式中的实例构造器可以设置为protected以允许子类派生。
- Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背。
- 如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。