LevelDB : AtomicPointer

leveldb裏有個AtomicPointer類:
inline void MemoryBarrier() {
  // Seehttp://gcc.gnu.org/ml/gcc/2003-04/msg01180.html for a discussion on
  // this idiom. Also seehttp://en.wikipedia.org/wiki/Memory_ordering.
  __asm__ __volatile__("": : : "memory");
}

class AtomicPointer {
 private:
  void* rep_;
 public:
  AtomicPointer() { }
  explicitAtomicPointer(void* p) : rep_(p) {}
  inlinevoid* NoBarrier_Load()const { return rep_; }
  inlinevoid NoBarrier_Store(void* v) { rep_ = v; }
  inlinevoid* Acquire_Load()const {
    void* result = rep_;
    MemoryBarrier();
    returnresult;
  }
  inlinevoid Release_Store(void* v) {
    MemoryBarrier();
    rep_ = v;
  }
};

首先這個原子指針的作用,不是字面上的其原子賦值的作用,因爲指針賦值原本就是原子操作。這裏其實涉及到了無鎖編程的概念,而無鎖編程又涉及內存屏障的概念。

這裏先推薦幾篇關於無鎖編程的博客,解釋的很好,建議想了解清楚的完整地閱讀完:

下面做一些理解後的總結:

無鎖編程的概念做一般應用層開發的會較少接觸到,因爲多線程的時候對共享資源的操作一般是用鎖來完成的。鎖本身對這個任務完成的很好,但是存在性能的問題,也就是在對性能要求很高的,高併發的場景下,鎖會帶來性能瓶頸。所以在一些如數據庫這樣的應用或者linux 內核裏經常會看到一些無鎖的併發編程。

鎖是一個高層次的接口,隱藏了很多併發編程時會出現的非常古怪的問題。當不用鎖的時候,就要考慮這些問題。主要有兩個方面的影響:編譯器對指令的排序和cpu對指令的排序。他們排序的目的主要是優化和提高效率。排序的原則是在單核單線程下最終的效果不會發生改變。單核多線程的時候,編譯器的亂序就會帶來問題,多核的時候,又會涉及cpu對指令的亂序。memory-ordering-at-compile-time和memory-reordering-caught-in-the-act裏提到了亂序導致的問題。

舉個例子,有下面的代碼:

a = b = 0;
//thread1
a = 1
b = 2

//thread2
if (b == 2) {
    //這時a是1嗎?
}

假設只有單核單線程 1的時候,由於a 和 b 的賦值沒有關係,因此編譯器可能會先賦值b然後賦值a,注意單線程的情況下是沒有問題的,但是如果還有線程2,那麼就不能保證線程2看到b爲2 的時候a就爲1。再假設線程1改爲如下的代碼:

a = 1
complier_fence()
b = 2

其中complier_fence()爲一條阻止編譯器在fence前後亂序的指令,x86/64下可以是下面的彙編語句,也可以由其他語言提供的語句保證。

asm volatile(“” ::: “memory”);

此時我們能保證b的賦值一定發生在a賦值之後。那麼此時線程2的邏輯是對的嗎?還不能保證。因爲線程2可能會先讀取a的舊值,然後再讀取b的值。從編譯器來看a和b之間沒有關聯,因此這樣的優化是可能發生的。所以線程2也需要加上編譯器級的屏障:

if (b == 2) {
    complier_fence()
    //這時a是1嗎?
}

加上了這些保證,編譯器輸出的指令能確保a,b之間的順序性。注意a,b的賦值也可以換成更復雜的語句,屏障保證了屏障之前的讀寫一定發生在屏障之後的讀寫之前,但是屏障前後內部的原子性和順序性是沒有保證的。

當把這樣的程序放到多核的環境上運行的時候,a,b賦值之間的順序性又沒有保證了。這是由於多核cpu在執行編譯器排序好的指令的時候還是會亂序執行。這個問題在memory-barriers-are-like-source-control-operations裏有很好的解釋。這裏不再多說。

同樣的,爲了解決這樣的問題,語言上有一些語句提供屏障的效果,保證屏障前後指令執行的順序性。而且,慶幸的是,一般,能保證cpu內存屏障的語句也會自動保證編譯器級的屏障。注意,不同的cpu的內存模型(即對內存中的指令的執行順序如何進行的模型)是不一樣的,很辛運的,x86/64是的內存模型是強內存模型,它對cpu的亂序執行的影響是最小的。

A strong hardware memory model is one in which every machine instruction comes implicitly withacquire and release semantics. As a result, when one CPU core performs a sequence of writes, every other CPU core sees those values change in the same order that they were written.

因此在x86/64上可以不用考慮cpu的內存屏障,只需要在必要的時候考慮編譯器的亂序問題即可。

回到leveldb裏的AtomicPointer,注意到其中幾個成員函數都是inline,如果不是inline,其實沒有必要加上內存屏障,因爲函數能夠提供很強的內存屏障保證。下面這段話摘自memory-ordering-at-compile-time:

In fact, the majority of function calls act as compiler barriers, whether they contain their own compiler barrier or not. This excludes inline functions, functions declared with thepure attribute, and cases where link-time code generation is used. Other than those cases, a call to an external function is even stronger than a compiler barrier, since the compiler has no idea what the function’s side effects will be. It must forget any assumptions it made about memory that is potentially visible to that function.

下面針對Acquire_Load和Release_Store假設一個場景:

//thread1:
Object.var1 = a;
Object.var2 = b;
Object.var2 = c;
atomicpointer.Release_Store(p);

//thread2
user_pointer = atomicpointer.Acquire_Load();
get Object.va1
get Object.var2
get Object.var3

結合之前的分析,可以很容易明白此時內存屏障保證了在線程1裏指針賦值之前對象的所有操作都已經完成,而在線程2裏面保證了取出指針後,纔會開始獲取新的對象內容。這符合程序的順序邏輯。

注意acquire,release模型適合單生產者和單消費者的模型,如果有多個生產者,那麼現有的保障是不足的,會涉及到原子性的問題。

最後附上一張an-introduction-to-lock-free-programming裏的圖,介紹了無鎖編程時相關的技術:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章