zoukankan      html  css  js  c++  java
  • 由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基础——面向对象三大特性:封装、继承、多态
    JVM内存
    50. Pow(x, n) (JAVA)
    47. Permutations II (JAVA)
    46. Permutations (JAVA)
    45. Jump Game II (JAVA)
    43. Multiply Strings (JAVA)
    42. Trapping Rain Water (JAVA)
    41. First Missing Positive (JAVA)
  • 原文地址:https://www.cnblogs.com/xuhuajie/p/11647164.html
Copyright © 2011-2022 走看看