線程同步機制之底層原子實現方式

臨界區

提到併發編程,首先就想到臨界區(critical section)這個概念,臨界區是線程中訪問臨界資源的一段需要互斥執行的代碼。

臨界資源

臨界資源是指線程之間共享的資源,但不同的執行序列結果不確定的,這也叫做競態條件(race condition)。

線程同步機制之底層原子實現方式

編程基本邏輯封裝原子操作-->信號量-->實現臨界區-->管程

保證同一時刻只有一個人拿到鑰匙(原子性)

多線程的三個特性:原子性、可見性、有序性 (詳細參考:多線程的三個特性這篇文章

如果只能一個人在屋子裡,那麼進去之後就鎖上,出來的時候再打開鎖;沒有鎖的人只能在外面等著。怎麼保證同一時刻只有一個線程進入臨界區,這個時候就需要保證線程的原子性。

  • 禁用硬件中斷:

我們知道,系統調用以及執行流程的切換都是依靠軟中斷。禁用中斷之後,進程(線程)就不會被切換出去,從而保證代碼段能執行結束。但壞處也很明顯,由於中斷被禁用,如果臨界區代碼一直執行,其他進程就沒機會執行了。而且,只能禁止單個CPU的中斷。

  • 基於軟件同步:

即基於代碼實現同步互斥,比較有名的是peterson算法,用來解決兩個進程對臨界區的互斥訪問問題。詳細參考:實現臨界區互斥的算法方法演變這篇文章

  • 基於原子操作原語的方法:

比較常見的原子操作指令包括 test and set、compare and swap。

示例:compare and swap

CAS,compare and swap的縮寫,中文翻譯成比較並交換,在intel的CPU中,使用cmpxchg指令。

CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值 。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該 位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前 值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。

很多編程語言鎖機制都是基於cas封裝,如下圖。

線程同步機制之底層原子實現方式

更多關於CAS詳細參考:CAS詳解和應用這篇文章。

話又說回來了,計算機怎麼保證這些指令的原子性的呢,繼續深挖,另一位登場?

CPU緩存模型

我們知道,CPU和物理內存之間的通信速度遠慢於CPU的處理速度,所以CPU有自己的內部緩存,根據一些規則將內存中的數據讀取到內部緩存中來,以加快頻繁讀取的速度。我們假設在一臺PC上只有一個CPU和一份內部緩存,那麼所有進程和線程看到的數都是緩存裡的數,不會存在問題;但現在服務器通常是多 CPU,更普遍的是,每塊CPU裡有多個內核,而每個內核都維護了自己的緩存,那麼這時候多線程併發就會存在緩存不一致性,這會導致嚴重問題。

線程同步機制之底層原子實現方式

線程同步機制之底層原子實現方式

帶有高速緩存的CPU執行計算的流程

  1. 程序以及數據被加載到主內存
  2. 指令和數據被加載到CPU的高速緩存
  3. CPU執行指令,把結果寫到高速緩存
  4. 高速緩存中的數據寫回主內存。

更多詳細請參考:cpu模型和JMM模型,jvm內存模型

處理器如何實現原子操作

32位IA-32處理器使用基於對緩存加鎖或總線加鎖的方式來實現多處理器之間的原子操作。

線程同步機制之底層原子實現方式

1 處理器自動保證基本內存操作的原子性

首先處理器會自動保證基本的內存操作的原子性。處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行裡進行16/32/64位的操作是原子的,但是複雜的內存操作處理器不能自動保證其原子性,比如跨總線寬度,跨多個緩存行,跨頁表的訪問。但是處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操作的原子性。

2 使用總線鎖保證原子性

第一個機制是通過總線鎖保證原子性。想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操作緩存了該共享變量內存地址的緩存。

處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔使用共享內存。

3 使用緩存鎖保證原子性(緩存一致性協議MESI

這個機制是通過緩存鎖定保證原子性。在同一時刻我們只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。

在同一時刻,我們只需要保證對某個內存地址的操作是原子性即可,頻繁使用的內存會緩存到處理器的L1,L2和L3高速緩存裡,那麼原子操作就可以直接在處理器內部緩存中進行,並不需要聲明總線鎖。所謂“緩存鎖定”是指內存區域如果被緩存在處理器的緩存行中,並且在Lock操作期間被鎖定(鎖定緩存行不鎖定總線),那麼當它執行鎖操作回寫到內存時,處理器不在總線上發出LOCK#信號,而是修改內部的內存地址,並允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據,當其他處理器回寫被鎖定的緩存行的數據時,會使緩存行無效。

但是有兩種情況下處理器不會使用緩存鎖定。第一種情況是:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行(cache line),則處理器會調用總線鎖定。第二種情況是:有些處理器不支持緩存鎖定。對於Inter486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。

以上兩個機制我們可以通過Inter處理器提供了很多LOCK前綴的指令來實現。比如位測試和修改指令BTS,BTR,BTC,交換指令XADD,CMPXCHG和其他一些操作數和邏輯指令,比如ADD(加),OR(或)等,被這些指令操作的內存區域就會加鎖,導致其他處理器不能同時訪問它。

緩存一致性協議MESI

MESI 協議是以緩存行(緩存的基本數據單位,在Intel的CPU上一般是64字節)的幾個狀態來命名的(全名是Modified、Exclusive、 Share or Invalid)。該協議要求在每個緩存行上維護兩個狀態位,使得每個數據單位可能處於M、E、S和I這四種狀態之一,各種狀態含義如下:

  • M:被修改的。處於這一狀態的數據,只在本CPU中有緩存數據,而其他CPU中沒有。同時其狀態相對於內存中的值來說,是已經被修改的,且沒有更新到內存中。
  • E:獨佔的。處於這一狀態的數據,只有在本CPU中有緩存,且其數據沒有修改,即與內存中一致。
  • S:共享的。處於這一狀態的數據在多個CPU中都有緩存,且與內存一致。
  • I:無效的。本CPU中的這份緩存已經無效。

詳細請參考:緩存一致性協議MESI這篇文章

參考

https://www.cnblogs.com/kkkkkk/p/5543799.html

https://blog.csdn.net/liangwenmail/article/details/80832580

https://blog.csdn.net/java1993666/article/details/77880651

https://www.cnblogs.com/yanlong300/p/8986041.html

https://blog.csdn.net/weixin_41835916/article/details/80601373

https://blog.csdn.net/vincent1007/article/details/53999136

https://blog.csdn.net/u014634338/article/details/76092710


分享到:


相關文章: