五、併發控制(1):執行緒的互斥

Eternitykc發表於2020-12-17

在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

看執行緒1,load(y)前面的是和y無關的store,所以作業系統允許store和load亂序。
這就使得實際可能是,執行緒1先執行load(y),執行緒2先執行load(x),然後分別進入(if !t1) goto,t1和t2同時進入臨界區,所以Peterson演算法出錯
Perterson演算法在現代計算機上實際上是錯誤的


在這裡插入圖片描述
do_sum程式碼中使用了內聯彙編,使得其真正迴圈相加,徹底阻止了編譯器的優化。
在這裡插入圖片描述
如果我們的狀態機在每一個時刻,確實是執行一個執行緒的一條彙編指令的話,我們應該看到正確的值4n
在這裡插入圖片描述
這個結果就推翻了上面寫的那個上節課的假設(狀態機每一時刻執行一條指令),而要改為這節課的假設——指令每個時刻只能執行一個load/store或者一些執行緒本地的計算,如果一個x86的指令裡面包含超過一個load/store,那麼就會拆成若干條指令執行,每一個指令最多隻能load一次或者store一次
那麼進入結果的解釋:

一條add指令拆分成 t=load(x);t++;store(x,t),這樣的話結果就位於區間(n,4n]之間了,但是為什麼也有小於n的情況。

下面進入進一步的解釋:

每一個處理器都有一個寫佇列store buffer,每次對x的store,不會立即寫入記憶體,而是首先寫入store buffer,執行時,那個佇列寫入x=1,x=2,x=3…。而我們在load的時候,只有當buffer裡沒有x時,才會去cpu 外面讀,若存在buffer中直接從buffer中讀取。store buffer裡的數值不是立即寫入記憶體,而是按照1,2,3…這個寫入的順序寫入記憶體。(例如x=1,實際上會寫出cpu )。而寫出cpu不是立即到達共享的記憶體中,而是到達cpu的快取中。如果這時執行緒1cpu1上已經完成了x=1,x=2,x=3的寫入,但是有可能所有的數值都還留在store buffer中,當我們的執行緒2在另一個cpu上執行load,會去共享記憶體中讀取,執行緒2會讀到sum=0,所以線上程1上完成了3次sum++的操作,但是線上程2看不到這個操作,因此sum<n就很合理了。

在這裡插入圖片描述
多處理器設計這麼複雜完全是為了單執行緒執行更快而設計的,像store buffercache,但是這就讓多執行緒的執行難以理解。


上一節課中,狀態機每一步執行一個指令的假設被推翻。一條指令會分解成若干條更小的指令,處理器的亂序會得到奇怪的結果。
在這裡插入圖片描述
如果硬體提供一點點更多的支援,在多處理器上實現互斥就變得非常容易。


在這裡插入圖片描述
如果硬體能提供一些指令幫我們就好了。
在這裡插入圖片描述

剛才提到,在共享記憶體上實現互斥,只能使用load,store,和執行緒本地計算。而load和store本身是存在能力上的缺陷的。
例如load環顧四周,只能看不能寫,而且每次只能看記憶體裡的一個地址
store在改變物理世界狀態的時候,不能讀,只能把眼睛蒙起來動手。這時候這個動手成功沒有,在動手的期間有沒有別的執行緒再動過手,這都是不知道的。
再加上現代多處理器上load和store執行時可能亂序,就更復雜了。

上面那個是有著明顯錯誤的鎖實現,如果狀態[IP1,IP2,locked]=[2,2,0](即if判斷成功進入了那個if,這個時候後面可以同時進入臨界區。


在這裡插入圖片描述
如果能保證load和store這中間不被打斷,就不會出現[2,2,0]的情況,就可以保證這段程式的正確性。
這種指令叫做:test-and-set

t=load(locked);
if (t == 0) store(locked,1);

實際上我們的硬體我們提供了很多的原子指令.

在這裡插入圖片描述

能保證原子性(這樣一條指令不會被打斷)
能保證順序以及多處理器間的可見性
能保證在原子指令之前所有的store會在其他的處理器之間可見,以及這樣的一條原子指令能保證在這個指令前後的load和store不能被亂序,所以保證了記憶體訪問的順序

在這裡插入圖片描述
上述程式碼在addq上新增了一個lock
在這裡插入圖片描述
和前面相比,就多了一個lock,然後指令開頭多了一個f0,表示這個指令需要鎖定,我們的CPU在訪問這條指令的時候就可以保證原子性,順序性和可見性,因此執行時的到了正確的結果4n。
命令time ./a.out,得到這個執行的結果是0.422s
而之前那個錯誤的指令是0.043s,在原子指令呼叫比較密集的情況下,可能會有10倍甚至更多的效能差距


為了實現互斥鎖,x86給我們提供了xchg指令(英文原單詞應該是exchange交換).
在這裡插入圖片描述
xchg將傳入的記憶體地址addr的值和傳入的引數newval的值做交換,他將瞬間完成*addr 和 newval數值的交換,不會被其他處理器打斷,原子完成
在這裡插入圖片描述
把執行緒想象成人,物理世界想象為共享記憶體,看上面的ppt,非常清楚。於是我們可以實現os課上的地一個鎖演算法——自旋鎖

所謂的自旋spin來自量子力學,量子不斷地旋轉,交換交換交換
直到換到了一把鑰匙,進入,所以成為自旋鎖。

在這裡插入圖片描述
可以改寫成一段非常精簡的程式碼:
可以把tabel重新命名為lockedKey=0NOTE=1
改寫成->:

while (1){
	int ret = xchg(locked,1);
	if (ret == 0) break;
}

可以進一步改寫成:

while( xchg(locked,1) ) ;

相關文章