单例模式

参考:C++ 单例模式总结与剖析

  • 对象性能模式
    • 面向对象很好地解决了”抽象”的问题,但会造成一定的代价.(虚函数,继承等)

动机

  • 在软件系统中,经常有这样一些特殊的类,必须保证他们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率
  • 具体运用场景如:
    • 设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动;
    • 数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;

定义

  • 单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;

要点

  • 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
  • 线程安全
  • 禁止赋值和拷贝
  • 用户通过接口获取实例:使用 static 类成员函数

代码实现

非线程安全的懒汉模式

  • 懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存。
#include <iostream>
// version1:
// with problems below:
// 1. thread is not safe
// 2. memory leak

class Singleton{
private:
    Singleton();
    ~Singleton(){}
    Singleton(Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Singleton* m_instance_ptr;
public:
    static Singleton* get_instance(){
        if(m_instance_ptr==nullptr){
              m_instance_ptr = new Singleton;
        }
        return m_instance_ptr;
    }
};

Singleton* Singleton::m_instance_ptr = nullptr;

存在问题

  • 线程安全的问题
    • 当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断m_instance_ptr是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来
    • 解决办法:加锁
  • 内存泄漏
    • 注意到类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏
    • 解决办法:使用共享指针

线程安全、内存安全的懒汉模式(双检查锁)

#include <memory> // shared_ptr
#include <mutex>  // mutex

// version 2:
// with problems below fixed:
// 1. thread is safe now
// 2. memory doesn't leak

class Singleton{
public:
    typedef std::shared_ptr<Singleton> Ptr;
    static Ptr get_instance(){
        // "double checked lock"
        if(m_instance_ptr==nullptr){
            std::lock_guard<std::mutex> lk(m_mutex);
            if(m_instance_ptr == nullptr){
              m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
            }
        }
        return m_instance_ptr;
    }

private:
    ~Singleton(){}
    Singleton(Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Ptr m_instance_ptr;
    static std::mutex m_mutex;
};

// initialization static variables out of class
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;

分析

  • 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
  • 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁(双检查锁);好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。
    • 单检查锁的代价过高,单锁的话线程每次访问都会加锁,但是在创建出来单例以后线程都是读操作,并不需要加锁了.这样导致每次只能一个线程读,造成效率下降.
      Singleton* Singleton::getInstance() {
        Lock lock;
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
        return m_instance;
      }
      

问题

  • 该方法会由于内存读写reorder导致不安全
    • 前提:线程时间片轮转,执行的是汇编指令
    • 所以对于c++代码在编译器进行优化以后,代码的执行顺序不一定是C++代码的逻辑顺序
  • 可能发生的错误情况
    • 在单列赋值的过程中,一般顺序是分配内存空间->调用构造器->范围内存地址,但可能编译器优化成分配内存空间->范围内存地址->调用构造器,这将可能导致threadB访问的时候直接范围未初始化好的单列,导致错误.
  • 解决方法: atomic

  • 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束;

atomic的可跨平台的双检查线程安全的单列模式

std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    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);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);//释放内存fence
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

基于C++11 magic static的懒汉单例模式

  • C++11标准中的Magic Static

    • If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
    • 如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
    • 这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。
  • static的声明周期(为什么可以这么写?)

    • 参考:What is the lifetime of a static variable in a C++ function?
    • The lifetime of function static variables begins the first time the program flow encounters the declaration and it ends at program termination. —- Motti
      • Since C++98 has no reference to multiple threads how this will be behave in a multi-threaded environment is unspecified
      • In C++11 statics are initialized in a thread safe way, this is also known as Magic Statics.
    • Compilers typically use a hidden flag variable to indicate if the local statics have already been initialized, and this flag is checked on every entry to the function. —-Roddy
#include <iostream>

class Singleton
{
public:
    static Singleton& get_instance(){
        static Singleton instance;
        return instance;
    }
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
};

分析

  • 通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
  • 不需要使用共享指针,代码简洁;
  • 注意在使用的时候需要声明单例的引用 Single& 才能获取对象。
    • 这样做并不好,理由主要是无法避免用户使用delete instance导致对象被提前销毁。还是建议大家使用返回引用的方式。