從C++單例形式到線程平安詳解。本站提示廣大學習愛好者:(從C++單例形式到線程平安詳解)文章只能為提供參考,不一定能成為您想要的結果。以下是從C++單例形式到線程平安詳解正文
先看一個最復雜的教科書式單例形式:
class CSingleton { public: static CSingleton* getInstance() { if (NULL == ps) {//tag1 ps = new CSingleton; } return ps; } private: CSingleton(){} CSingleton & operator=(const CSingleton &s); static CSingleton* ps; }; CSingleton* CSingleton::ps = NULL;
有2個要點:
1.private的結構函數和=操作符,用於避免類外的實例化和被復制;
2.static的類指針和get辦法。
在大少數單線程狀況下,以上代碼大都會運轉得很好,除非遇到中綴:
1.當順序運轉到tag1 處觸發了中綴;
2.中綴處置順序恰調用的也是getInstance函數。
可想而知,這和多線程的狀況相似,假定線程A 運轉到tag1處,還沒來得及new,此時ps依然是NULL,線程B(或中綴處置順序) 同時也運轉到此經過if判別,那麼將會實例化2個CSingleton對象,顯然是不對的。
為理解決上述問題,自但是然,最容易想到也最常用的辦法是加鎖,因而getInstance改成這樣:
static CSingleton* getInstance() { lock();//偽代碼 if (NULL == ps) { ps = new CSingleton; } return ps; }
加了鎖當前貌似處理了上述問題,但也異樣帶來了新的問題:假如順序四處是諸如:
CSingleton::instance()->aaaa(); CSingleton::instance()->bbbb(); CSingleton::instance()->cccc();
這樣的調用,除了第一次的lock()有用外,前面的都是在做無用功,lock()的代價說大不大,但在某些狀況下還是會進步順序延遲,這對追求完滿的順序猿來說是完全無法承受的。
於是乎,咱想出了一個方法:
static CSingleton* getInstance() { if (NULL == ps)//這裡加了次判別,只要第一次才會為true而調用lock() { lock();//偽代碼 if (NULL == ps) { ps = new CSingleton; } } return ps; }
很久當前我才知道,這個辦法有個很矮小上的名字,叫做雙重反省鎖定形式,簡稱DCLP(Double Checked Locking Pattern)。
DCLP很好地處理了屢次調用不用要的lock()。
但是,你們以為這樣就完了?too young。。
DCLP在多線程下依然存在2個基本問題:
1.順序的指令執行順序不確定;
2.編譯器優化問題。
先說2,在某些編譯器下,以上的兩個if判別只會執行一個,甚至一個都不執行,緣由是編譯器以為至多有一個if判別是多余的,它自動協助我們優化了代碼。
再說1,ps = new CSingleton; 這條語句會被拆分為這樣的三個步驟執行:
1.為要new的對象開拓一塊內存;
2.結構該對象,填入這塊內存;
3.將ps指針指向這塊內存。
以上三個步驟,2和3的順序是不確定的,能夠先2後3,也能夠先3後2。。。
實踐執行時能夠是這樣的:
static CSingleton* getInstance() { if (NULL == ps) { lock();//偽代碼 if (NULL == ps) { //偽代碼 ps = xx;//step 3 new sizeof(CSingleton);//step 1 new CSingleton;//step 2 } } return ps; }
假如編譯器按上述順序執行代碼,思索如下情況:
線程A 執行到step 1還未執行前面的step 2,此時ps非空,但其指向的內存外面的內容還未被結構出來,於此同時線程B 進入這個函數,判別ps非空直接前往ps,但是調用者此時訪問的ps內存實踐內容CSingleton還沒被結構呢,這是一塊地址正確大小正確但外部數據不明的東西,當然會出錯(調用者普通這麼調用:CSingleton::getInstance()->aa(); CSingleton::getInstance()->bb(); CSingleton::getInstance()->cc();........此時的aa,bb,cc是啥玩意兒?)。
這也是為什麼加上volatile關鍵字依然不可以處理同步問題,volatile只處理了編譯器優化問題,卻無法控制機器指令執行順序。
很遺憾的是,C/C++自身在設計時是不思索多線程問題的,也就是說,要處置多線程問題還要順序猿自己想方法填坑。。
說了這麼多,我們要討論的問題依然沒有處理,慶幸的是,C++ 11提供了內存柵欄技術來處理這個問題,這裡不贅述,有興味的讀者可以自己搜索材料看看,不過是一些api調罷了。
那麼,C++ 11 以前的代碼如何處理這個問題呢?很不幸,並沒有很好的處理方案,一種可行的方案是,順序中不要四處這麼調用這個單例對象:
CSingleton::getInstance()->aa(); CSingleton::getInstance()->bb(); CSingleton::getInstance()->cc();
而是在順序開端就初始化緩存這個單例對象:
CSingleton* const g_ps = CSingleton::getInstance();//順序一開端就緩存這個單例對象 g_ps->aa(); g_ps->bb(); g_ps->cc();
但是如此帶來的問題是順序一開端就實例化了這個單例對象,對象在整個順序的聲明周期存在,這貌似叫餓漢式,而之前那種叫懶漢式,孰輕孰重,只要依據實踐狀況取捨了。
以上就是為大家帶來的從C++單例形式到線程平安詳解全部內容了,希望大家多多支持~