• C++设计模式---单例模式


    单例模式应该是最常用的设计模式了,也很容易理解。但是这里面却有一些坑。


    单例模式的使用场景

    游戏当中需要很多游戏配置,这个配置只需要一个实例,就可以采用单例模式。

    单例模式注意的一些坑

    饿汉模式

    饿汉模式需要注意一个点,那就是对象创建不要在头文件中,而是应该放在.cpp文件中,否则会在链接时冲突。
    比如在instanc.hpp中使用饿汉模式创建对象,在test.cpptest1.cpp中进行包含这个头文件,此时单例模式就会因为有两个.cpp文件都存在,这两个文件中就会因为存在同一个同名对象而造成链接冲突。
    在这里插入图片描述

    通常饿汉模式对象的创建在main函数之后,是非常安全的。

    懒汉模式的问题

    指令重排

    饿汉模式通常都是进行双重nullptr判断,然后new对象:
    在这里插入图片描述

    在多线程的情况下,m_instance = new GameConfig();这一行代码通常分为三步:1.先调用malloc分配内存,2.执行GameConfig的构造函数来初始化内存,3.将m_instance 指向这一块内存。
    但是在CPU内部执行过程中,很有可能因为编译器的优化打乱上面的三个步骤,也就是指令重排。
    这也就意味着,线程1有可能先将m_instance 赋值,然后再调用构造函数初始化内存,那线程2拿到的就是一个没有被初始化的m_instance 。

    关于指令重排的内容可以查看这一篇文章什么是指令重排序和内存屏障,看完你就懂了

    解决指令重排

    要解决指令重排,方法有很多。最简单的方法就是,在main函数执行开始,就先执行一次getInstance

    另一种则是使用C++11的内存屏障

    #include 
    #include 
    #include 
    using namespace std;
    
    namespace hjl_project1
    {
        class GameConfig
        {
        public:
            static GameConfig *getInstance()
            {
                GameConfig *tmp = m_instance.load(std::memory_order_relaxed);
                std::atomic_thread_fence(std::memory_order_acquire); //获取内存屏障
                if (tmp == nullptr)
                {
                    lock_guard<mutex> gcguard(my_mutex);
                    tmp = m_instance.load(std::memory_order_relaxed);
                    if (m_instance == nullptr)
                    {
                        tmp = new GameConfig;
                        std::atomic_thread_fence(std::memory_order_release); //释放内存屏障
                        m_instance.store(tmp, std::memory_order_relaxed);
                    }
                }
                return m_instance;
            }
    
        private:
            GameConfig() {}
            GameConfig(const GameConfig &tmpobj);
            GameConfig &operator=(const GameConfig &tmpobj);
            ~GameConfig() {}
            static atomic<GameConfig *> m_instance;
            static mutex my_mutex;
        };
        std::atomic<GameConfig *> GameConfig::m_instance;
        mutex GameConfig::my_mutex;
    }
    int main()
    {
        using namespace hjl_project1;
        GameConfig *g_gc = GameConfig::getInstance();
    }
    
    • 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
    • 42
    • 43
    • 44

    最后一种解决办法,就是利用C++11 static 特性:如果当变量在初始化的时候,并发同时进⼊声明语句,并发线程将会阻塞等待初始化结束。

    这也是最简洁的一种实现方式:

    namespace hjl_project2
    {
        class GameConfig
        {
        public:
            static GameConfig &getInstance()
            {
                static GameConfig instance;
                return instance;
            }
    
        private:
            GameConfig() {}
            GameConfig(const GameConfig &tmpobj);
            GameConfig &operator=(const GameConfig &tmpobj);
            ~GameConfig() {}
        };
    
    }
    int main()
    {
        using namespace hjl_project2;
        GameConfig &g_gc = GameConfig::getInstance();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这个版本具有以下优势

    1. 利⽤静态局部变量特性,延迟加载;
    2. 利⽤静态局部变量特性,系统⾃动回收内存,⾃动调⽤析构函数;
    3. 静态局部变量初始化时,没有 new 操作带来的cpu指令reorder操作;
    4. c++11 静态局部变量初始化时,具备线程安全;

    单例模式之间相互引用的问题

    如果两个单例模式之间相互引用,则程序结束析构时,析构顺序并不确定,比如现在有两个单例Log、GameConfig,如果GameConfig的析构函数中需要利用Log对象记录一些信息,但是Log比GameConfig提前析构,此时就会出现问题,导致代码执行出错。

    要解决这个问题,就要注意不要在单例模式的析构函数中,引用其他单例模式

  • 相关阅读:
    【Apifox】为什么如此受青睐,此篇文章和大家分享
    Solidity智能合约事件(event)
    Optional非空判断
    开发模型的特点对照表
    leetcode 376 摆动序列
    可解释机器学习- InterpretML的使用|interpretable machine learning- InterpretML tutorial
    Adaptive AUTOSAR 学习笔记 7 - 应用设计和 Manifest
    地图结构 | 图解占据栅格地图原理(附Matlab建图实验)
    Golang中的闭包详解
    微信小程序抢红包高并发设计
  • 原文地址:https://blog.csdn.net/qq_52670477/article/details/126882585