單例的Double check問題和CPU動態調度共存時的線程安全問題。


改編自《程序員的自我修養》主要是爲了記錄和整理一下自己的思路。

一種典型的單例的實現方式是:


volatile T* pInstance = NULL;
T* getInstance(){
	if(!pInstance){
		lock();
		if(!pInstance){
			T* temp = new T();
			pInstance = temp;
		}
		unlock();
	}
	return pInstance
}


說明:這裏使用了個temp,而不是直接賦值給pInstance。我的理解:是因爲就素直接那麼寫編譯之後,也是相當於有個temp(寄存器)的,就是先放在寄存器,再放到pInstance對應的內存中。

1. 爲什麼lock放在內層?
在內層爲了介紹多線程情況下上鎖帶來的開銷。因爲在單實例已經創建之後,對單實例的指針僅僅是讀操作,不會產生衝突,如果讀操作加鎖,相當於脫褲子放屁。
2. 爲什麼要再次檢查?
假設在pInstance還爲空的時候,T1和T2同時執行lock,其中一個線程new了一個實例,然後unlock。另一個線程進入臨界區,有一次new了一個實例。那就會創建多個實例。這顯然會導致潛在的問題。
3. 這段代碼會有什麼問題?
在CPU沒有動態調度的時候,這段代碼是不會有問題的。
但是在CPU有動態調度的時候,
如果順序執行,順序是:分配內存,調用構造函數的那段代碼,寫指針對應的內存。
如果cpu動態調度了,順序可能是:分配內存,寫指針對應內存,調用構造函數。 (爲什麼能這麼調度,我的理解是:寫指針到對應的內存邏輯上不依賴於調用構造函數)CPU有很多實質上的寄存器供它自己玩兒動態調度,不用擔心它調度不過來這麼多代碼。動態調度的reservation station可能會達到一百多個!)
第一個線程調用getInstance,發現指針null,進入臨界區,new對象,給pInstance賦值,還沒有調用構造函數,第二個線程被調度,調用getInstance,發現不爲空,然後函數返回,第二個線程開始使用一個沒有初始化的單實例對象。
這就是問題。

問題的根源在於:cpu調度了沒有依賴的構造函數和指針賦值操作。(因爲兩個操作都只是讀temp指針,並不對temp指針進行賦值。)
解決辦法,阻止cpu調度這兩端代碼。
使用CPU普遍提供的barrier指令。barrier指令會組織cpu將它之前的指令被調度到它之後,也會組織它之後的指令被調度到它之前。
PowerPC上的最終代碼如下:
#define barrier __asm__ volatile ("lwsync")
volatile T* pInstance = NULL;
T* getInstance(){
	if(!pInstance){
		lock();
		if(!pInstance){
			T* temp = new T();
			barrier();
			pInstance = temp;
		}
		unlock();
	}
	return pInstance
}

感觸:
1. 併發的問題真是複雜。
2. 程序的真正執行過程可能並不像語言上形容的那樣一樣。比如說,C語言的順序執行,到了動態調度就不是順序執行了。
要真正弄清問題,還真的要想到某條語句可能對應的硬件執行流程。
3. 又印證了我的那個總結的觀點:很多問題都是爲了所謂的優化帶來的,不管是用起來便利還是性能啊之類的。
a. 你用起來便利就可能會讓別有意圖的人也用起來便利。
b. 有時候性能優化的時候過於aggressive,有點單刀直入的感覺,往往會忽略某些東西~

4. CPU的動態調度,推廣到其他方面,真的會把你玩死!哈哈


轉載註明來自:http://blog.csdn.net/cheetach119/article/details/8990084

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章