1. 程式人生 > >由std::once_call 引發的單例模式的再次總結,基於C++11

由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

 

 

 

 

 

 

 

 

 

         

 

 

       &n