1. 程式人生 > >執行緒同步(1):原子操作,記憶體屏障,鎖綜述

執行緒同步(1):原子操作,記憶體屏障,鎖綜述

原子操作,記憶體屏障,

1.原理:CPU提供了原子操作、關中斷、鎖記憶體匯流排,記憶體屏障等機制;OS基於這幾個CPU硬體機制,就能夠實現鎖;再基於鎖,就能夠實現各種各樣的同步機制(訊號量、訊息、Barrier等等等等)。

2.所有的同步操作最基礎的理論就是原子操作。記憶體屏障,鎖都是為了保證在不同的平臺或者是CPU型別下的原子操作。

3.原子操作在單核,單執行緒/無中斷,且編譯器不優化的情況下是確定的,是按照C/C++程式碼順序執行的,所以不存在非同步問題

  解釋一下這幾個知識點為什麼會引起非同步操作:

    首先了解一下cpu處理指令的步驟:

           1.早起的處理器為有序處理器,指令處理順序

:

a.讀取指令

  b.執行指令如果暫存器可寫就從記憶體取出a的資料到暫存器,暫存器不可寫就等待

          c.暫存器處理指令

          d.將暫存器結果存入記憶體

           2.現在的處理器大多數為亂序處理器,處理順序:

            a.讀取指令

b.指令被劃分到指令佇列

  c.指令在佇列中等待,如果暫存器可寫就從記憶體取出a的資料到暫存器,暫存器不可寫就等待

          d.暫存器處理指令

          e.將執行結果存入佇列(而不是立即寫入暫存器堆)

f.只有當所有更早的請求執行的指令結果被寫入記憶體之後,執行的結果才會被存入記憶體

(執行結果重排序,讓執行看起來是有序的)

        那麼問題來了:1.一條簡單的a++語句究竟會有這麼多條指令,而這一組指令是可以在任意時候非同步執行的(共享資料)

a.單核多執行緒情況下,執行緒是存在中斷的,中斷的時候cpu呼叫另一執行緒的同一指令組,所以是可能出現交叉執行的可能,也就是說單執行緒或者關掉中斷可以解決非同步問題,但很多時候這種做法並不實際          

                        b.多核多執行緒情況下共享資料被多個核並行處理,不論哪一種處理器都存在同時執行的可能,這就導致了非同步問題

  其中以前做遊戲伺服器開發的時候,一開始不理解很多遊戲伺服器架構為什麼業務執行緒都是一條執行緒處理,因為遊戲中很多涉及到共享資料,所以避免不了的要使用各種鎖,但是鎖多了問題反而更多。                     

       c.現在的編譯器都具有優化及自動優化功能,優化之後可能會對共享變臉的訪問順序進行調整,可能會造成與預期不相符的結果。

4.記憶體屏障的作用:a.在編譯時:拒絕編譯器優化屏障前後的指令,防止記憶體亂序訪問;b.在執行時:告訴記憶體地址匯流排共享資料地址的資料必須同步(當多個執行緒同時將一個共享資料地址的資料載入到佇列裡的時候,先完成處理從cpu到記憶體的時候總是通知其他執行緒跟新佇列中的該共享資料,從而保證一致性)

  Memory barrier 常用場合包括:

1.實現同步原語(synchronization primitives

2.實現無鎖資料結構(lock-free data structures

3.驅動程式

  記憶體屏障包含4中基本型別:寫屏障,資料依賴屏障(常與寫屏障成對出現),讀屏障,通用記憶體屏障(包含讀寫屏障)

  記憶體屏障還有兩種隱式的屏障變種:LOCKUNLOCK操作(表面上這兩個操作的實際用途和原子操作裡面的Lock解釋有區別,原子操作裡面的lock是鎖記憶體匯流排,這裡面的lock是保證執行的執行順序嚴格按照lock前,lock中,lock後的順序執行)

記憶體屏障按照使用層次可以分為

· 編譯器屏障。

· CPU記憶體屏障。

· MMIO write屏障。

所以:記憶體屏障只是一種執行緒同步的手段,並不會阻塞執行緒;僅保證了程式碼執行順序和多核競爭情況下的資料一致性

5.:從上面可以看出記憶體屏障並不是鎖,而鎖是使用了記憶體屏障實現的一種使用者層的同步處理方式,鎖使用的彙編原語有LOCK,UNLOCK是記憶體屏障的一種隱式形式,它們都是LOCK操作和UNLOCK操作的變種,所以幾乎所有的鎖都使用了記憶體屏障,

  鎖包含了:

原子鎖:使用了鎖匯流排的方式實現原子操作

自旋鎖:while等待,不可搶佔的單CPU核心下是無效的,有軟中斷的情況下,必須使用時本地軟中斷失效的方法。自旋鎖更像是一種使用者層控制的while等待處理

讀寫鎖讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作

互斥鎖:沉睡/休眠等待,所以互斥鎖比自旋鎖排程耗時。

訊號量:用於同一時刻有多個個例項能獲取鎖,可用於表示同時有多少個client請求允許訪問同一個資料塊,允許鎖個數設定為1的時候就是互斥鎖.

                讀寫訊號量:對同時擁有的讀者數不受限制,只能一個寫者,寫者發現不需要寫的時候降級為讀者。

順序鎖:用於能夠區分讀與寫的場合,並且是讀操作很多、寫操作很少,寫操作的優先權大於讀操作。

讀拷貝鎖:RCUread-copy-update(RCU也是用於能夠區分讀與寫的場合,並且也是讀多寫少,但是讀操作的優先權大於寫操作)

rcuclassic:禁止核心搶佔的

rcupreempt:允許核心搶佔的,實時性更高,和rcuclassic相反

rcutree:和rcuclassic類似

BKL(大核心鎖): 整個核心只有一把這樣的鎖,一旦一個程序獲得大核心鎖,進入了被它保護的臨界區,不但該臨界區被鎖住,所有被它保護的其它臨界區都將無法訪問,直到該程序釋放大核心鎖

:以下為摘錄整理部分。

詳解:

第一章:從硬體層面解釋原因

 1.概念:

CPU基本原理開始說起,系統性能提升必須以瞭解CPU基本原理為前提條件,另外,CPU Cache工作原理也是提升系統整體效能的非常重要的方面,所以本文拿出專門章節對其原理進行了詳細介紹。

1. 基本概念

在現代CPU體系設計結構中,一般提供了下面幾種機制來提升系統的整體效能:

1)匯流排加鎖、cache一致性管理:以實現對系統記憶體的原子操作、序列化指令(serializing instructions。這些指令僅對pentium4,Intel Xeon, P6,Pentium處理器有效)

2)處理器晶片內建的高階可程式設計中斷控制器(APIC)

3)二級快取(level 2, L2) 對於Pentium4,Intel Xeon, P6處理器,L2 cache已經緊密的封裝到了處理器中。而Pentium,Intel486提供了用於支援外部L2 cache的管腳。

4)超執行緒技術:它能夠讓一個處理器核心併發的執行兩個或兩個以上的指令流。

這些機制在對稱多處理系統(symmetric-multiprocessing, SMP)中是極其有用的。然而,RMI這些多核系統中,這些機制也是適用的。

多處理器機制的設計必須滿足下面的需求:

1)保持系統記憶體的完整性(coherency): 當兩個或多個處理器試圖同時訪問系統記憶體的同一地址時,必須有某種通訊機制或記憶體訪問協議來提升資料的完整性,以及在某些情況下,允許一個處理器臨時鎖定某個記憶體區域。

2)保持快取記憶體的一致性當一個處理器訪問另一個處理器快取中的資料時,必須要得到正確的資料。如果這個處理器修改了資料,那麼所有的訪問這個資料的處理器都要收到被修改後的資料。

3)允許以可預知的順序寫記憶體在某些情況下,從外部觀察到的寫記憶體順序必須要和程式設計時指定的寫記憶體順序相一致。

4)在一組處理器中派發中斷處理當幾個處理器正在並行的工作在一個系統中時,有一個集中的機制是必要的,這個機制可以用來接收中斷以及把他們派發到某一個適當的處理器。

5)採用現代作業系統和應用程式都具有的多執行緒和多程序的特性來提升系統的效能

 2.一致性原因

在多執行緒程式設計中,為了保證資料操作的一致性,作業系統引入了鎖機制,用於保證臨界區程式碼的安全。通過鎖機制,能夠保證在多核多執行緒環境中,在某一個時間點上,只能有一個執行緒進入臨界區程式碼,從而保證臨界區中操作資料的一致性。

所謂的鎖,說白了就是記憶體中的一個整型數,擁有兩種狀態:空閒狀態和上鎖狀態。加鎖時,判斷鎖是否空閒,如果空閒,修改為上鎖狀態,返回成功;如果已經上鎖,則返回失敗。解鎖時,則把鎖狀態修改為空閒狀態。

看起來很簡單,大家有沒有想過,OS是怎樣保證這個鎖操作本身的原子性呢?舉個例子,在多核環境中,兩個核上的程式碼同時申請一個鎖,兩個核同時取出鎖變數,同時判斷說這個鎖是空閒狀態,然後有同時修改為上鎖狀態,同時返回成功。。。兩個核同時獲取到了鎖,這種情況可能嗎?

廢話,當然是不可能,可能的話,我們使用鎖還有啥意義。但是,咦?等等,雖然我知道肯定不可能,但是你剛才說的貌似還有點道理,看來OS實現這個鎖還不是看起來這麼簡單,還是有點道道的。

為了弄明白鎖的實現原理,我們首先看看如果OS不採用任何其他手段,什麼情況下會導致上鎖失敗?假如我們把加鎖過程用如下偽碼錶示:

1read lock

2、判斷lock狀態;

3、如果已經加鎖,失敗返回;

4、把鎖狀態設定為上鎖;

5、返回成功。

明白彙編的同學一看就明白上述每一步都能對應到一條彙編語句,所以我們可以認為每一步本身是原子的。

那麼什麼情況能夠導致兩個執行緒同時獲取到鎖呢?

1、中斷:假設執行緒A執行完第一步,發生中斷,中斷返回後,OS排程執行緒B,執行緒B也來加鎖並且加鎖成功,這時OS排程執行緒A執行,執行緒從第二步開始執行,也加鎖成功。

2、多核:當然了,想想上面舉的例子,描述的就是兩個核同時獲取到鎖的情況。

既然明白鎖失敗的原因,解決手段就很明確了:

先考慮單核場景:

1、既然只有中斷才能把上鎖過程打斷,造成多執行緒操作失敗。我先關中斷不就得了,在加鎖操作完成後再開中斷。

2、上面這個手段太笨重了,能不能硬體做一種加鎖的原子操作呢?能,大名鼎鼎的“test and set”指令就是做這個事情的。

通過上面的手段,單核環境下,鎖的實現問題得到了圓滿的解決。那麼多核環境呢?簡單嘛,還是“test and set”不就得了,這是一條指令,原子的,不會有問題的。

真的嗎,單獨一條指令能夠保證該指令在單個核上執行過程中不會被中斷打斷,但是兩個核同時執行這個指令呢?。。。我再想想,硬體執行時還是得從記憶體中讀取lock,判斷並設定狀態到記憶體,貌似這個過程也不是那麼原子嘛。對,多個核執行確實會存在這個問題。

怎麼辦呢?首先我們得明白這個地方的關鍵點,關鍵點是兩個核會並行操作記憶體而且從操作記憶體這個排程來看“test and set”不是原子的,需要先讀記憶體然後再寫記憶體,如果我們保證這個記憶體操作是原子的,就能保證鎖的正確性了。

確實,硬體提供了鎖記憶體匯流排的機制,我們在鎖記憶體匯流排的狀態下執行test and set操作,就能保證同時只有一個核來test and set,從而避免了多核下發生的問題。

總結一下,在硬體層面,CPU提供了原子操作、關中斷、鎖記憶體匯流排的機制;OS基於這幾個CPU硬體機制,就能夠實現鎖;再基於鎖,就能夠實現各種各樣的同步機制(訊號量、訊息、Barrier等等等等)。