1. 程式人生 > >lock-free介紹

lock-free介紹

無鎖程式設計 / lock-free / 非阻塞同步

無鎖程式設計,即不使用鎖的情況下實現多執行緒之間的變數同步,也就是在沒有執行緒被阻塞的情況下實現變數的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。

實現非阻塞同步的方案稱為“無鎖程式設計演算法”( Non-blocking algorithm)。

lock-free是目前最常見的無鎖程式設計的實現級別(一共三種級別)。

為什麼要 Non-blocking sync ?

使用lock實現執行緒同步有很多缺點:

* 產生競爭時,執行緒被阻塞等待,無法做到執行緒實時響應。

* dead lock。

* live lock。

* 優先順序翻轉。

* 使用不當,造成效能下降。

 

如果在不使用 lock 的情況下,實現變數同步,那就會避免很多問題。雖然目前來看,無鎖程式設計並不能替代 lock。

實現級別

非同步阻塞的實現可以分成三個級別:wait-free/lock-free/obstruction-free。

 

wait-free

是最理想的模式,整個操作保證每個執行緒在有限步驟下完成。

保證系統級吞吐(system-wide throughput)以及無執行緒飢餓。

截止2011年,沒有多少具體的實現。即使實現了,也需要依賴於具體CPU。

 

lock-free

允許個別執行緒飢餓,但保證系統級吞吐。

確保至少有一個執行緒能夠繼續執行。

wait-free的演算法必定也是lock-free的。

 

obstruction-free

在任何時間點,一個執行緒被隔離為一個事務進行執行(其他執行緒suspended),並且在有限步驟內完成。在執行過程中,一旦發現數據被修改(採用時間戳、版本號),則回滾。

也叫做樂觀鎖,即樂觀併發控制(OOC)。事務的過程是:1讀取,並寫時間戳;2準備寫入,版本校驗;3校驗通過則寫入,校驗不通過,則回滾。

lock-free必定是obstruction-free的。

CAS原語

LL/SC, atom read-modify-write

如果CPU提供了Load-Link/Store-Conditional(LL/SC)這對指令,則就可以輕鬆實現變數的CPU級別無鎖同步。

LL [addr],dst:從記憶體[addr]處讀取值到dst。

SC value,[addr]:對於當前執行緒,自從上次的LL動作後記憶體值沒有改變,就更新成新值。

上述過程就是實現lock-free的 read-modify-write 的原子操作。

 

CAS (Compare-And-Swap)

LL/SC這對CPU指令沒有實現,那麼就需要尋找其他演算法,比如CAS。

CAS是一組原語指令,用來實現多執行緒下的變數同步。

在 x86 下的指令CMPXCHG實現了CAS,前置LOCK既可以達到原子性操作。截止2013,大部分多核處理器均支援CAS。

CAS原語有三個引數,記憶體地址,期望值,新值。如果記憶體地址的值==期望值,表示該值未修改,此時可以修改成新值。否則表示修改失敗,返回false,由使用者決定後續操作。

 

Bool CAS(T* addr, T expected, T newValue) 
 { 
      if( *addr == expected ) 
     { 
          *addr =  newValue; 
           return true; 
     } 
     else 
           return false; 
 }

 

 

ABA 問題

thread1意圖對val=1進行操作變成2,cas(*val,1,2)。

thread1先讀取val=1;thread1被搶佔(preempted),讓thread2執行。

thread2 修改val=3,又修改回1。

thread1繼續執行,發現期望值與“原值”(其實被修改過了)相同,完成CAS操作。

 

使用CAS會造成ABA問題,特別是在使用指標操作一些併發資料結構時。

 

解決方案

ABAʹ:新增額外的標記用來指示是否被修改。

語言實現

Java demo

AtomicInteger atom = new AtomicInteger(1);

boolean r = atom.compareAndSet(1, 2);

 

C# demo

int i=1;

Interlocked.Increment(ref i);

 

記憶體模型(Memory Model)對細粒度鎖的影響

在多執行緒系統中,當多個執行緒同時訪問共享的記憶體時,就需要一個規範來約束不同的執行緒該如何與記憶體互動,這個規範就稱之為記憶體模型(Memory Model)。

順序一致性記憶體模型(Sequential Consistency Memory Model)則是記憶體模型規範中的一種。在這個模型中,記憶體與訪問它的執行緒保持獨立,通過一個控制器(Memory Controller)來保持與執行緒的聯絡,以進行讀寫操作。在同一個執行緒內的,讀寫操作的順序也就是程式碼指定的順序。但多個執行緒時,讀寫操作就會與其他執行緒中的讀寫操作發生交錯。

如上圖中所示,Thread 1 中在寫入 Value 和 Inited 的值,而 Thread 2 中在讀取 Inited 和 Value 的值到 Ri 和 Rv 中。由於在記憶體控制器中發生重排(Memory Reordering),最終的結果可能有很多種情況,如下表所示。

順序一致性記憶體模型非常的直觀,也易於理解。但實際上,由於該模型在記憶體硬體實現效率上的限制,導致商用的 CPU 架構基本都沒有遵循該模型。一個更貼近實際的多處理器記憶體模型更類似於下圖中的效果。

也就是說,每個 CPU 核都會有其自己的快取模型,例如上圖中的 Level 1 Cache 和 Level 2 Cache,用以快取最近使用的資料,以提升存取效率。同時,所有的寫入資料都被緩衝到了 Write Buffer 緩衝區中,在資料在被重新整理至快取前,處理器可以繼續處理其他指令。這種架構提升了處理器的效率,但同時也意味著我們不僅要關注 Memory,同時也要關注 Buffer 和 Cache,增加了複雜性。

上圖所示為快取不一致問題(Incoherent Caches),當主存(Main Memory)中儲存著 Value=5,Inited=0 時,Processor 1 就存在著新寫入 Cache 的值沒有被及時重新整理至 Memory 的問題,而 Processor 2 則存在著讀取了 Cache 中舊值的問題。

顯然,上面介紹著記憶體重排和快取機制會導致混亂,所以實際的記憶體模型中會引入鎖機制(Locking Protocol)。通常記憶體模型會遵循以下三個規則:

  • Rule 1:當執行緒在隔離狀態執行時,其行為不會改變;
  • Rule 2:讀操作不能被移動到獲取鎖操作之前;
  • Rule 3:寫操作不能被移動到釋放鎖操作之後;

Rule 3 保證了在釋放鎖之前,所有寫入操作已經完成。Rule 2 保證要讀取記憶體就必須先獲取鎖,不會再有其他執行緒修改記憶體。Rule 1 則保證了獲得鎖之後的操作行為是順序的。

在體現鎖機制(Locking Protocol)的價值的同時,我們也會意識到它所帶來的限制,也就是限制了編譯器和 CPU 對程式做優化的自由。

我們知道,.NET Framework 遵循 ECMA 標準,而 ECMA 標準中則定義了較為寬鬆的記憶體訪問模型,將記憶體訪問分為兩類:

  • 常規記憶體訪問(Ordinary Memory Access)
  • 易變記憶體訪問(Volatile Memory Access)

其中,易變記憶體訪問是特意為 "volatile" 設計,它包含如下兩個規則:

  1. 讀和寫操作不能被移動到 volatile-read 之前;
  2. 讀和寫操作不能被移動到 volatile-write 之後;

對於那些沒有使用 "lock" 和 "volatile" 的程式片段,編譯器和硬體可以對常規記憶體訪問做任何合理的優化。反過來講,記憶體系統僅需在應對 "lock" 和 "volatile" 時採取快取失效和重新整理緩衝區等措施,這極大地提高了效能。

順序一致性(Sequential Consistency)的要求描述了程式程式碼描述的順序與記憶體操作執行的順序間的關係。多數程式語言都提供順序一致性的支援,例如在 C# 中可以將變數標記為 volatile。

A volatile read has "acquire semantics" meaning that the read is guaranteed to occur prior to any references to memory that occur after the read instruction in the CIL instruction sequence. 
A volatile write has "release semantics" meaning that the write is guaranteed to happen after any memory references prior to the write instruction in the CIL instruction sequence.