由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

 

 

 

 

 

 

 

 

 

         

 

 

        


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM