一個偶然的機會,知道了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