01-单例模式

单例模式

概念

单例模式(singleton)的理念非常简单,即应用程序中只能有一个特定组件的实例。

经典实现

单例模式的经典实现如下:

class Singleton
{
protected:
    Singleton() { /* 初始化... */ }

public:
    static Singleton& getInstance()
    {
        // 从C++11开始, 这种写法是线程安全的
        static Singleton instance;
        return instance;
    }

    // 显式删除多余的构造函数和赋值运算符, 防止意外创建实例
    Singleton(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator= (const Singleton&) = delete;
    Singleton& operator= (Singleton&&) = delete;
};

单例与线程安全

多线程单例

从C++11开始,采用上面的写法是线程安全的,这意味着如果多个线程同时调用getInstance(),我们永远不会碰到创建两次实例的情况。

在C++11之前,需要使用一种称为 双重校验锁 的方式来实现单例模式:

// atomic<Singleton*> m_instance;
Singleton& getInstance()
{
    // 第一次校验
    if (!m_instance)
    {
        Lock lock;
        // 第二次校验
        if (!m_instance)
        {
            m_instance = new Singleton;
        }
    }
}

每线程单例

可能的情况是,程序中所有线程间不需要共享一个单例,而是 每个线程都需要一个单例

构建过程和之前的经典实现一样,只是我们现在要为静态函数中的变量加上thread_local声明,让每个线程都拥有一个单例:

static Singleton& getInstance()
{
    thread_local Singleton instance;
    return instance;
}

每线程单例解决了某些特殊问题:

  1. 假设有一个依赖关系图如下:

    1
    A --依赖--> B --依赖--> C
    2
    A --依赖--> C

    其中C是单例对象。现在有20个线程,每个线程都创建了一个A的实例,组件A依赖C两次(直接依赖和通过B间接依赖)。如果单例对象C有状态,且在每个线程中的状态均不同,那么单例对象C就不应该是全局的,而是每线程单例的。

  2. 在每线程单例中,我们不必担心多线程单例的线程安全问题。因此可以使用map而不必使用concurrent_hash_map(好像是tbb里的东西)。

单例与控制反转

显式将某个组件变为单例的方式具有明显的侵入性,而如果决定在某一时刻不再将某个类作为单例,最终又会付出高昂的代价。这里可以采用一种约定:负责组件的函数并不直接控制组件的生命周期,而是外包给控制反转(Inversion of Control, IoC)容器

如果使用Boost.DI的依赖注入框架,定义单例组件的代码如下:

auto injector = di::make_injetor(
    di::bind<IFoo>.to<Foo>.in(di::singleton),
    // 其他配置...
);

每当需要具有IFoo类型成员的组件时,我们使用Foo的单例实例来初始化该组件。

单例与环境上下文

单例还可以与环境上下文(Context)思想结合。例如我们要管理某个数据,它是全局的且不同时间的状态可能不同,这时候就需要用到单例+环境上下文。如果要返回上一个状态,则需要用栈来存储上下文。

  • 单例模式并不完全令人厌恶,但是如果不小心使用,它们会破坏应用程序的可测试性和可重构性。
  • 如果必须使用单例模式,请尝试避免直接使用它,将其指定为依赖项,并保证所有依赖项都是从应用程序的某个唯一的位置(如控制反转容器)获取或初始化的。

单态模式

单态模式(Monostate)是单例模式的一种变体。单态模式行为上类似于单例,但看起来像一个普通的类:

class Printer
{
public:
    int get_id() const {return m_sId;}
    void set_id(int val) {m_sId = val;} 
private:
    static int m_sId;
}

可以发现这个类操作的都是静态数据,并通过getter和setter使用

单态模式允许继承和多态,开发者可以更容易地定义和控制其生命周期。其最大的优势在于,它允许我们使用并修改在当前系统中已经使用的对象,使其以单态模式的方式在系统中运行。

单态模式的缺点是:它是一种侵入性方法,并且静态成员的使用意味着它总是会占据内存空间。最大的缺点在于,它做了过于乐观的假设,即外界总是会通过getter和setter方法来访问单态类的成员。如果直接访问它们,重构实现几乎注定要失败。

参考资料

  • 《C++20设计模式 可复用的面向对象设计方法》
  • youngyangyang04/kama-DesignPattern: 卡码网-23种设计模式精讲,每种设计模式都配套代码练习题,支持 Java,CPP,Python,Go (github.com)