• 由std::once_call 引发的单例模式的再次总结,基于C++11


             一个偶然的机会,知道了std::once_call这个东西。

             了解了下,std::once_call支持多线程情况下的某函数只执行一次。咦,这个不是恰好符合单例模式的多线程安全的困境吗?

             单例模式,经常需要手写的经典面试题之一,很考验面试者的底子和水平。需要考虑的细节很多,其中多线程安全也是一个点。

             本篇博文再次总结下单例模式,并且尽可能详细与完整,建议mark,面试前再回忆下(毕竟工作中直接有代码可以抄)。

             单例模式,在本人看来是全局变量的一种C++封装。

             常规的C语言中,经常会在文件开头定义一坨全局变量,有些还加上extern来支持变量的跨文件访问。确实难以维护,而且当项目庞大了,

    有可能发生变量被偷偷修改的情况,导致一些奇怪难以排查的bug。

             单例模式,则提供了一个供全局访问的类,包含了一系列全局访问的变量与方法,经过组织之后,变量的维护更清晰。一般以管理类居多。

    带Manager类名的类往往都是单例模式实现的。

             常规的单例模式的设计,仅能通过Instance方法(有些喜欢getInstance)类指针或者得到类实例(往往是引用,毕竟只有1个实例)。因此,

    第一点要考虑的就是禁止构造函数、拷贝构造与赋值函数。如果需要释放资源,一般不允许调用delete 方法,最多对外提供Releace(有些喜欢Destroy)

    方法来内部释放资源换。因此析构函数也要禁用。通用的单例模式的第一份装逼代码先写好。

             

    #define SINGLETON_CTOR(x) 
         private:
                x() = default;
                x(const x&)=delete;
                x& operator=(const x&)=delete;
                ~x()=default;

              因为不能通过构造函数得到类实例,因此类的Instance方法必须是static(即绑定到类对象设计本身,不属于类实例的方法)的。

              有些人会区分饿汉式或者懒汉式的,面试时有时会紧张,写不全代码,先记住最简单又安全的实现。

           

    class Singleton
    {
        SINGLETON_CTOR(Singleton);
    public:
        static Singleton& Instance()
        {
            static Singleton _instance;
            return _instance;
        }    
    };

              静态局部变量会在第一次使用时初始化,多次调用被会编译器忽略,生命周期是程序的运行区间,并且是多线程安全的。

              因为静态局部变量是分配在全局静态数据区(不是堆或者栈),内存一直都在(默认全部填0,但不占程序大小bss段)。

             在我看来算属于饿汉式的,即程序运行期间就需要内存。

      ok,我们看看其他变体实现。

             

    class Singleton2
    {
        SINGLETON_CTOR(Singleton2);
    public:
        static Singleton2* Instance()
        {
            static Singleton2 _instance;
            return &_instance;
        }
    };

             有些人喜欢指针多一点。。。,就返回指针好了。

             当然,既然如此简单,我们可以通过宏再加工一下,方便他人使用。

    #define SINGLETON_INSTACNCE(x) SINGLETON_CTOR(x)
        public:
        static x* Instance()
        {static x _instance; return &_instance;}
    
    class SingletonOnceMore
    {
        SINGLETON_INSTACNCE(SingletonOnceMore);
    public:
        void fun(){}
    };
    class SingletonTwiceMore
    {
        SINGLETON_INSTACNCE(SingletonTwiceMore);
    public:
        void fun(){}
    };
    
    
    SingletonOnceMore::Instance()->fun();
    SingletonTwiceMore::Instance()->fun();

           

    class Singleton3
    {
        SINGLETON_CTOR(Singleton3);
    public:
        static Singleton3* Instance()
        {
            return &_instance;
        }
    
        static Singleton3 _instance;
    };
    
    Singleton3 Singleton3::_instance;  //这个得放cpp中,不然编译报错

          静态成员变量也是ok的。

    到这里为止,都是饿汉式的实现,接下来实现下懒汉式。

    class SingletonNotGood
    {
        SINGLETON_CTOR(SingletonNotGood);
    public:
        static SingletonNotGood* Instance()
        {
            if (!_pInstance)
            {
                _pInstance = new SingletonNotGood;
            }
            return _pInstance;
        }
        static SingletonNotGood* _pInstance;
    };
    
    SingletonNotGood* SingletonNotGood::_pInstance;//这个得放cpp中,不然编译报错,静态成员默认赋null。

    这是最简单的一种懒汉式实现。即看看指针存在否,不存在new一下。但存在一些问题。

    1、内存无法正常释放

    2、多线程不安全

    尽管存在这些问题,但是如果你的管理类的生命周期与程序一样长,就可以不用考虑内存泄漏,毕竟操作系统会在程序退出时自动回收。(不过小心socket,可能导致不能正常关闭的问题 )

    然后如果没有多线程的困扰(比如很多管理类带有Init方法,在main函数的入口不远处先调用Init方法来实例化),那么这个简单的方法项目中还是可以用的。

    当然,本文既然是总结,我们还得继续。一种简单的优化后如下:

    #include <mutex>
    
    class SingletonNotGood
    {
        SINGLETON_CTOR(SingletonNotGood);
    public:
        static SingletonNotGood* Instance()
        {
            std::lock_guard<std::mutex> lock_(m_cs);
            if (!_pInstance)
            {
                _pInstance = new SingletonNotGood;
            }
            return _pInstance;
        }
        static void Release()
        {
            std::lock_guard<std::mutex> lock_(m_cs);
            if (!_pInstance)
            {
                delete _pInstance;
                _pInstance = nullptr;
            }
        }
    private:
        static SingletonNotGood* _pInstance;
        static std::mutex m_cs;
    };
    
    SingletonNotGood* SingletonNotGood::_pInstance;//这个得放cpp中,不然编译报错,静态成员默认赋null。
    std::mutex SingletonNotGood::m_cs;//这个得放cpp中,不然编译报错,

    这里我们还可以使用 Double-Checked Locking Pattern (DCLP) 来减少锁的竞争机会,因为大部分情况下,_pInstance都是非空的。

    static SingletonNotGood* Instance()
        {
            if (!_pInstance)  //读操作1
            {
                std::lock_guard<std::mutex> lock_(m_cs);  //只有空的情况下才加锁
                if (!_pInstance)
                {
                    _pInstance = new SingletonNotGood;  //写操作2
                }
            }
            return _pInstance;
        }

    尽管这个术语非常高上大,很多博客也会提及,但其实细究起来,它并不是线程安全的。

    注意到_pInstance = new SingletonNotGood,是一个写操作,前面有一个无锁的读操作。当真正的写操作进行时,前面的读操作存在脏读情况。

    _pInstance = new SingletonNotGood,表面上一个语句,展开后由

    1、malloc 一块内存,地址复制到_pInstance 

    2、针对_pInstance 地址上调用placement new进行类的构造。

    当多线程情况下,一个线程有可能进行了1之后,另外个线程进来后,判断非空,进行类对象的访问,导致crash。

    如果这样写的项目没有遇到崩溃,大概率都是在main的某个地方提前实例化过了(如管理类很多有init方法,调用了就实例化了)。

    这个崩溃的场景的概率真的很小 ,需要多线程恰好同时调用Instance,并且某一个线程执行了malloc后,分出时间片,另外个线程拿到了未构造的类实例进行操作。

    但如果面试过程中,你能指出这一点,也是加分项吧。。。。

    好的,优化后的单例looks good了,但还是有内存泄漏的风险,用户确实忘了Release了,有时,也不敢乱Release(因为你不知道还有其他人要弄否)。想要自动管理

    内存释放?当然可以的。方法一:加一个垃圾收集类。

    class SingletonNotGood
    {
        SINGLETON_CTOR(SingletonNotGood);
    public:
        static SingletonNotGood* Instance()
        {
            if (!_pInstance)  //读操作1
            {
                std::lock_guard<std::mutex> lock_(m_cs);  //只有空的情况下才加锁
                if (!_pInstance)
                {
                    _pInstance = new SingletonNotGood;  //写操作2
                }
            }
            return _pInstance;
        }
        static void Release()  
        {
            std::lock_guard<std::mutex> lock_(m_cs);
            if (!_pInstance)
            {
                delete _pInstance;
                _pInstance = nullptr;
            }
        }
    
    private:
        struct GarbageCollect
        {
            ~GarbageCollect()
            {
                if (!_pInstance)
                {
                    delete _pInstance;
                    _pInstance = nullptr;
                }
            }
        };
    
    private:
        static SingletonNotGood* _pInstance;
        static std::mutex m_cs;
        static GarbageCollect gc;
    };
    
    SingletonNotGood* SingletonNotGood::_pInstance;//这个得放cpp中,不然编译报错,静态成员默认赋null。
    std::mutex SingletonNotGood::m_cs;//这个得放cpp中,不然编译报错,
    SingletonNotGood::GarbageCollect SingletonNotGood::gc;//这个得放cpp中,不然编译报错,

    当然由于静态变量的空间是在全局内存区,其空间的释放是在程序结束才进行释放的。而在程序结束时,系统会自动回收该程序申请的空间。

    gc的析构函数释放静态实例时,也是在程序结束时才会调用的。所以这里写的内存释放意义不大。当然对于那些在程序结束后不自动回收空间的系统,还是需要写空间回收的。

    方法二,采用智能指针。

    #include <memory>
    class SingletonUsePtr
    {
        SINGLETON_CTOR(SingletonUsePtr);
    public:
        static SingletonUsePtr& Instance()
        {
            if (!_ptr)  //读操作1
            {
                std::lock_guard<std::mutex> lock_(m_cs);  //只有空的情况下才加锁
                if (!_ptr)
                {
                    _ptr.reset(new SingletonUsePtr);
                }
            }
            return *_ptr;
        }
    private:
        static std::unique_ptr<SingletonUsePtr> _ptr;
        static std::mutex m_cs;
    };
    
    std::unique_ptr<SingletonUsePtr> SingletonUsePtr::_ptr;//这个得放cpp中,不然编译报错,
    std::mutex SingletonUsePtr::m_cs;//这个得放cpp中,不然编译报错,

    这里使用shared_ptr也可以,不过shared_ptr占用的内存和内部复杂度(额外的有个block的概念用于存放引用计数等)稍大点。

    推荐返回Singleton & 。用了智能指针就得放弃裸指针。

    接下来,终于可以引出std::once_call再次优化以去掉额外的锁了。

    class SingletonUsePtr2
    {
        SINGLETON_CTOR(SingletonUsePtr2);
    public:
        static SingletonUsePtr2& Instance()
        {
            static std::once_flag s_flag;
            std::call_once(s_flag, [&]() {
                _ptr.reset(new SingletonUsePtr2);
            });
    
            return *_ptr;
        }
    private:
        static std::unique_ptr<SingletonUsePtr2> _ptr;
    };

    这个相对来说,是最简单的安全可靠的懒汉式实现了。有兴趣的也可以封装成宏,方便他人使用。

    最后,再使用模板实现一份,采用curiously recurring template pattern,CRTP,不详细展开了,您坚持看到现在累了,我也写的累了 = =(主要原因)。

    //采用模板再实现一次,
    //使用方法 class YourSingleton: public SingletonBase<YourSingleton>
    template<typename T>  //T 是子类
    class SingletonBase
    {
        SINGLETON_CTOR(SingletonBase);  //这个还是可以用的
    public:
        static T&  Instance()
        {
            static T t;   //饿汉式
            return t;
        }
    };
    
    //再加上今天的学习的std::once_call实现懒汉式
    template<typename T>  //T 是子类
    class SingletonBaseLazy
    {
        SINGLETON_CTOR(SingletonBaseLazy);  //这个还是可以用的
    public:
        static T&  Instance()
        {
            static std::once_flag flag;
            std::call_once(flag, [&](){_ptr.reset(new T); });
            return *_ptr;
        }
        static std::unique_ptr<T> _ptr;
    };
    template<typename T>  
    std::unique_ptr<T> SingletonBaseLazy<T>::_ptr;
    
    
    #include <iostream>
    class YourSingleton : public SingletonBaseLazy < YourSingleton >
    {
    public:
        void test()
        {
            std::cout << "hello word" << std::endl;
        }
    };
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        YourSingleton::Instance().test();
        YourSingleton::Instance().test();
        return 0;
    }

    代码已上传 https://github.com/xuhuajie-NetEase/SingletonMode

             

            

  • 相关阅读:
    Java验证码
    Java内存泄漏问题
    Java常见异常总结
    Java编码与乱码问题
    Spring:源码解读Spring IOC原理
    Quartz 定时邮件发送多个备份文件
    spring-boot 多线程
    java并发基础
    Java多线程中wait语句的具体使用技巧
    多线程的死锁
  • 原文地址:https://www.cnblogs.com/xuhuajie/p/11647164.html
Copyright © 2020-2023  润新知