五、併發控制(1):執行緒的互斥
看執行緒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的快取中。如果這時執行緒1
在cpu1
上已經完成了x=1,x=2,x=3的寫入,但是有可能所有的數值都還留在store buffer中
,當我們的執行緒2
在另一個cpu上執行load
,會去共享記憶體
中讀取,執行緒2會讀到sum=0
,所以線上程1上完成了3次sum++的操作,但是線上程2看不到這個操作,因此sum<n
就很合理了。
多處理器設計這麼複雜完全是為了單執行緒執行更快而設計的,像store buffer
,cache
,但是這就讓多執行緒的執行難以理解。
上一節課中,狀態機每一步執行一個指令的假設被推翻。一條指令會分解成若干條更小的指令,處理器的亂序會得到奇怪的結果。
如果硬體提供一點點更多的支援,在多處理器上實現互斥就變得非常容易。
如果硬體能提供一些指令幫我們就好了。
剛才提到,在共享記憶體上實現互斥,只能使用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
重新命名為locked
,Key
=0
,NOTE
=1
改寫成->:
while (1){
int ret = xchg(locked,1);
if (ret == 0) break;
}
可以進一步改寫成:
while( xchg(locked,1) ) ;
相關文章
- 聊聊併發(五)——執行緒池執行緒
- 執行緒的互斥鎖執行緒
- 【併發技術03】傳統執行緒互斥技術—synchronized執行緒synchronized
- 併發工具類(三)控制併發執行緒的數量 Semphore執行緒
- 執行緒同步與互斥:互斥鎖執行緒
- golang 限流器,控制併發,執行緒池Golang執行緒
- Java併發基礎03:傳統執行緒的互斥技術—synchronizedJava執行緒synchronized
- Go高效併發 10 | Context:多執行緒併發控制神器GoContext執行緒
- Java併發程式設計,互斥同步和執行緒之間的協作Java程式設計執行緒
- Java併發 之 執行緒池系列 (1) 讓多執行緒不再坑爹的執行緒池Java執行緒
- 畫江湖之 PHP 多執行緒開發 【執行緒安全 互斥鎖】PHP執行緒
- 畫江湖之 PHP 多執行緒開發 [執行緒安全 互斥鎖]PHP執行緒
- 多執行緒(2)-執行緒同步互斥鎖Mutex執行緒Mutex
- 66.QT-執行緒併發、QTcpServer併發、QThreadPool執行緒池QT執行緒TCPServerthread
- Linux之執行緒互斥鎖Linux執行緒
- Java併發指南1:併發基礎與Java多執行緒Java執行緒
- Rust的併發執行緒 - ibraheemRust執行緒
- python基礎執行緒-管理併發執行緒Python執行緒
- 多執行緒併發篇——如何停止執行緒執行緒
- 多執行緒與高併發(五)final關鍵字執行緒
- Java多執行緒—執行緒同步(單訊號量互斥)Java執行緒
- Java併發(四)----執行緒執行原理Java執行緒
- Java併發(一)----程式、執行緒、並行、併發Java執行緒並行
- [Java併發]執行緒的並行等待Java執行緒並行
- 大牛聊Java併發程式設計原理之 執行緒的互斥與協作機制Java程式設計執行緒
- 併發與多執行緒之執行緒安全篇執行緒
- 多執行緒與高併發(二)執行緒安全執行緒
- 併發程式設計之多執行緒執行緒安全程式設計執行緒
- JAVA多執行緒併發Java執行緒
- Java 併發:執行緒、執行緒池和執行器全面教程Java執行緒
- Linux多執行緒的使用一:互斥鎖Linux執行緒
- MySQL 配置InnoDB的併發執行緒MySql執行緒
- Java執行緒的併發工具類Java執行緒
- 啃碎併發(五):Java執行緒安全特性與問題Java執行緒
- 多執行緒(五)---執行緒的Yield方法執行緒
- Java併發實戰一:執行緒與執行緒安全Java執行緒
- java多執行緒與併發 - 執行緒池詳解Java執行緒
- 執行緒控制之休眠執行緒執行緒