跳至主要內容

《C++设计模式》视频_李建忠

LincZero大约 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。但此时构造器还未被调用,会出错
  • 建议

    • 不要用,不安全,容易出错
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?注意对双检查锁的正确实现。