• CPP design pattern Singleton


    Singleton模式概念:
    保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    然而真正实现起来却并非易事,甚至有些棘手。棘手之处在于删除问题,如何删除Singleton实例?谁来删除?何时删除?

    在[DP]中,并未探讨有关析构和删除的问题,这是GoF的疏忽。此模式虽小,内涵却多,随着观察的深入,问题便突显出来。之后,John Vlissides(GoF之一)在Pattern hatching(1998)一书中探讨了这个问题。

    和工厂模式等不同,Singleton对象需要自己对「所有权」进行管理,用户无权删除实例。

    Singleton对象从创建到删除之间便是其生命期,然而,我们只知道创建时间,而不知其删除时间,也就无法管理所有权。

    事实上就算我们不进行释放操作,程序在结束之时,操作系统也会将进程所用的内存完全释放,并不会产生内存泄漏。然而,若Singleton产生的实例在构造时申请过广泛的资源(通常是内核资源,如套接字,信号量,互斥体,事件),便会产生资源泄漏。解决办法就是在程序关闭时正确地删除Singleton对象,此时,删除时机便至关重要。

    Singleton模式还有一个问题是多线程支持,不过C++11之后并非主要问题。

    总结一下,Singleton模式虽然易于表达和理解,但却难以实现。关键在于三点:创建,删除,多线程。其中删除是最棘手的问题,能同时满足这三点的实现,便能适应几乎所有情况。

    1 Singleton的唯一性
    Singleton模式是一种经过改进的全局变量,重点集中于“产生和管理一个独立对象,并且不允许产生另外一个这样的对象”。

    也就是说具有唯一性,因此一切能够再次产生同样对象的方式都应该被杜绝,比如:构造函数,拷贝构造,移动构造,赋值操作符。

    [DP]中的定义如下:

     1class Singleton
     2{
     3public:
     4
     5    static Singleton* Instance() {
     6        if(!pInstance_) {
     7            pInstance_ = new Singleton;
     8        }
     9        return pInstance_;
    10    }
    11
    12private:
    13    Singleton();
    14    Singleton(const Singleton&) = delete;
    15    Singleton& operator=(const Singleton&) = delete;
    16    Singleton(Singleton&&) = delete;
    17    Singleton& operator=(Singleton&&) = delete;
    18    ~Singleton();
    19
    20    static Singleton* pInstance_;
    21};
    22Singleton* Singleton::pInstance_ = 0;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这种方式的确满足了唯一性,用户除了从Instance()获取对象之外,别无他法。且用户无法意外删除对象,因为析构函数也被私有了(不过依然可以由返回的指针删除之,所以最好以引用返回)。

    最大的问题在于此法只满足三大关键之一:创建,对于删除和多线程都不满足。

    所以,在满足唯一性的前提下,Singleton的实现还应设法管理产生对象的生命期和多线程环境下的访问安全。

    2 隐式析构的Singleton
    1节的Singleton只有创建,没有删除,这可能会导致资源泄漏,一种解决方法是使用隐式析构。

    C++的static成员变量的生命期伴随着整个程序,在程序关闭时由编译器负责销毁。于是我们可以创建一个static嵌套类,在其析构函数中释放singleton对象。

    具体的实现如下:

     1class singleton
     2{
     3public:
     4    static singleton& instance()
     5    {
     6        if(!pInstance_)
     7        {
     8            destroy_.create();
     9            std::cout << "create\n";
    10            pInstance_ = new singleton;
    11        }
    12        return *pInstance_;
    13    }
    14
    15private:
    16    static singleton* pInstance_;
    17
    18    // embedded class
    19    // implicit deconstructor
    20    struct singleton_destroyer {
    21        ~singleton_destroyer() {
    22            if(pInstance_) {
    23                std::cout << "singleton destroyed!\n";
    24                delete pInstance_;
    25            }
    26        }
    27        void create() {}
    28    };
    29    static singleton_destroyer destroy_;
    30
    31protected:
    32    singleton() = default;
    33    virtual ~singleton() {}
    34    Singleton(const Singleton&) = delete;
    35    Singleton& operator=(const Singleton&) = delete;
    36    Singleton(Singleton&&) = delete;
    37    Singleton& operator=(Singleton&&) = delete;
    38};
    39
    40singleton* singleton::pInstance_ = nullptr;
    41singleton::singleton_destroyer singleton::destroy_;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    此时singleton将能够具有摧毁功能,此时再加入线程处理也并非难事,如何保证线程安全可以参见7.5节。

    3 Meyers singleton
    隐式析构的Singleton的确是一种不错的实现方式,然而C++实现Singleton最简单而有效的方法还是Meyers singleton,实现如下:

     1class singleton
     2{
     3public:
     4    static singleton& instance() {
     5        static singleton obj;
     6        return obj; 
     7    }
     8
     9private:
    10    Singleton();
    11    Singleton(const Singleton&) = delete;
    12    Singleton& operator=(const Singleton&) = delete;
    13    Singleton(Singleton&&) = delete;
    14    Singleton& operator=(Singleton&&) = delete;
    15    ~Singleton();
    16};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    此法并未使用动态分配和静态指针,而是借用了一个局部静态变量。

    和上一个方法一样,该法也是到第一次执行时才会初始化对象(网上有些地方称之为懒汉式,然而在正统C++设计模式相关书籍中,皆未出现过此种叫法),它返回的是引用,所以用户无法对返回的对象进行delete操作(暴力转换法不算:))。此外,在C++11之后,这种方法也是线程安全的,所以对于大多情况下,这是最简单也是最有效的实作法,仅有两行代码。

    ** Meyers singleton 关键之处是让static对象定义在函数内部,变成局部static变量**

    如果是把 static对象定义成 Singleton的私有static成员变量,然后getInstance()去返回这个成员即:

    class Singleton {
    public:
        static Singleton& getInstance() {
            return inst;
        }
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    
        // 其他数据函数
        // ...
    
    private:
        Singleton() { ... }
        static Singleton inst;
        // 其他数据成员
        // ...
    };
    Singleton Singleton::inst;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    虽然它也是 先getInstance()再访问,但这种不是Meyers’ Singleton!

    那么为什么Meyers推荐的是第一种的呢?

    原因是这解决了一类重要问题,那就是static变量的初始化顺序的问题。

    C++只能保证在同一个文件中声明的static变量的初始化顺序与其变量声明的顺序一致。但是不能保证不同的文件中的static变量的初始化顺序。

    然后对于单例模式而言,不同的单例对象之间进行调用也是常见的场景。比如我有一个单例,存储了程序启动时加载的配置文件的内容。另外有一个单例,掌管着一个全局唯一的日志管理器。在日志管理初始化的时候,要通过配置文件的单例对象来获取到某个配置项,实现日志打印。

    这时候两个单例在不同文件中各自实现,很有可能在日志管理器的单例使用配置文件单例的时候,配置文件的单例对象是没有被初始化的。这个未初始化可能产生的风险指的是C++变量的未初始化,而不是说配置文件未加载的之类业务逻辑上的未初始化导致的问题。

    而Meyers’ Singleton写法中,单例对象是第一次访问的时候(也就是第一次调用getInstance()函数的时候)才初始化的,但也是恰恰因为如此,因而能保证如果没有初始化,在该函数调用的时候,是能完成初始化的。所以先getInstance()再访问 这种形式的单例 其关键并不是在于这个形式。而是在于其内容,局部static变量能保证通过函数来获取static变量的时候,该函数返回的对象是肯定完成了初始化的!

    编译器会负责管理局部静态变量,当程序结束时进行析构,看样子一切安好。然而,当生成的多个Singleton对象具有依赖关系时,Meyers singleton就无能为力了(隐式析构Singleton亦如此)。

    假设我们有四个Singleton,Director(导演)、Scene(场景)、Layer(图层)、Log(日志)。我们处于一个简单的游戏中,该游戏只有一个导演,一个场景,一个图层,图层上会有若干精灵。我们依次创建,到Layer时初始化失败了,此时创建一个Log来记录崩溃原因。之后程序关闭,执行期的相关机制会来摧毁静态对象,摧毁的顺序是LIFO,所以会先摧毁Log,然后是Layer。但若是Scene关闭失败,此时再想向Log记录崩溃原因,由于Log已被摧毁,所以返回的用只是一个“空壳”,之后的操作便会发生不确定性行为,这种问题称为dead-reference。
    dead-reference问题的原因在于C++并不保证静态对象析构的顺序,因此具有依赖关系的多个Singleton无法正确删除。

    使用局部静态变量的另一个缺点是难以通过派生子类来扩展Singleton,因为instance创建的始终都是singleton类型的对象。不过可以通过泛型编程来解决,见5节。

    4 可以复活的Singleton
    要满足具有依赖关系的Singleton,其中一个思路是当需要再次用到已经被执行期处理机制删除的Singleton对象时,使其死灰复燃,复活的Singleton需要自己负责删除。

    所以首先需要增加一个标志来记录singleton是否已被摧毁,若已摧毁,则使其复生;若未摧毁,则创建Singleton对象。

    大体上的代码如下:

     1class singleton
     2{
     3public:
     4    static singleton& instance()
     5    {
     6        if(!pInstance_)
     7        {
     8            if(destroyed_)
     9                on_dead_reference(); // 出现dead-reference,重生singleton
    10            else
    11                create(); // 创建singleton
    12        }
    13        return *pInstance_;
    14    }
    15private:
    16    static singleton *pInstance_;
    17    static bool destroyed_; // 记录是否已被摧毁
    18};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    那么具体就是要看on_dead_reference和create这两个函数是如何处理的,create和之前一样,可以直接使用Meyers singleton:

    1static void create()
    2{
    3    static singleton obj;
    4    pInstance = &obj;
    5}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当singleton对象析构时,也就是摧毁实例时,改变destroyed_标记(默认为false),将其改为摧毁状态:

    1virtual ~singleton()
    2{
    3    pInstance = nullptr;
    4    destroyed_ = true;
    5}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而如何让对象死灰复燃呢?我们可以借助placement new,在对象死去的“空壳”上重新创建对象。

    1void on_dead_reference()
    2{
    3    create();
    4    new(pInstance_) singleton;
    5    std::atexit(kill_singleton);
    6    destroyed_ = false;
    7}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里的关键在于atexit函数,该函数可以注册一些函数,注册的函数将会在程序结束之时被调用。事实上static成员变量的默认释放也是通过此函数完成的,编译器会自动生成释放函数向atexit注册,释放机制是LIFO。

    此时我们是通过placement new重新构造了singleton对象,所以需要自己释放,因此定义一个kill_singleton函数:

    1static void kill_singleton()
    2{
    3    pInstance_->~singleton();
    4}
    
    • 1
    • 2
    • 3
    • 4

    只需调用析构函数释放便可。

    reference: https://mp.weixin.qq.com/s/ufDK34vWC6yTSkS3TmucWw
    https://www.zhihu.com/question/56527586
    https://github.com/lkimuk/okdp/blob/master/singleton/singleton.hpp

  • 相关阅读:
    第五章:双指针与离散化的映射
    [枚举]Stormwind 2022杭电多校第8场 1011
    Python手写基因编程
    Scala 的学习笔记
    2021蓝桥杯真题 小蓝卡片 数学+二分查找+前缀和优化
    47从零开始用Rust编写nginx,配对还有这么多要求!负载均衡中的路径匹配
    KALILINUX MSF中kiwi(mimikatz)模块的使用
    数据库存储过程
    风险管理案例题
    vant 按需导入 vue2
  • 原文地址:https://blog.csdn.net/weixin_42244181/article/details/125470316