內存序列-memor order

內存序

今天看內存序看的要崩潰了,太亂了,不同人的博客常常出現前後不一致的情況。這個工作只是臨時起意,也不是什麼意義重大的工作,因此也沒有尋找更權威的資料,最主要的參考是cplusplus上的reference,如果有錯誤之外,真心希望您來指正一下,自己真的逐個字死磕類型的。

爲何需要內存序

在實際的程序運行過程中,如果不使用任何的同步原語,那麼很多變量的執行結果,乃至程序的最終行爲是無法預料的。具體來說,影響主要有以下幾個點:

  1. 編譯器指令的重新排列:編譯器出於優化的考慮,會對生成的指令代碼進行重新排列,而排列的代碼可能在多線程運行中引起不可預料的結果。要注意的是,在單線程"順序執行"的情況下,這種重排並不會影響程序結果。
  2. cpu cache:例如對內存的某個變量的寫操作,可能先寫入了cache,而並沒有立刻被其他線程看到
  3. 指令的非原子性:例如某個操作需要涉及多條指令,而我們希望這個操作是個原子操作,不能被其他線程看到操作的中間結果(因爲中間結果可能無任何意義)。

爲了能夠準確的控制內存讀寫操作的可見性,保證多線程的讀寫情況下變量結果的正確,需要一定的內存序的語義支持。傳統的方案常採用機制,以控制多線程之間的並行訪問,但是它的代價相對較高。然而使用內存序也就是無鎖策略,常可以獲得更高的性能,這也是很多“無鎖數據結構”採用的招式。

C++11針對atomic原子變量的幾種內存序

如下的幾種內存序遵循了從“鬆散”到“嚴格”的策略,所謂“鬆散”就是對內存執行序列的限制比較少,而“嚴格”就是限制比較多。

memory_order_relaxed

最好記。鬆散內存序,多線程之間不會有任何的同步和內存序的限制,也就是說編譯器可以任意的指定指令的重排策略。

memory_order_consume

最難理解。consume顧名思義就是“消費”,也就是讀(load)操作,它要求:當前線程所有的依賴此load的原子變量的所有讀寫操作決不能被重排到load操作的前面

這個裏面關鍵的兩個字是“依賴”。也就是說,假定當前讀取的變量是x,

如下面的代碼,2不能被重排到1的前面,

// 線程1
x依賴atm

// 線程2
std::atmoic<int> atm = 0;
z = atm.load(memory_order_consume) // 1
x = 1; // 2
y = 2; // 3

memory_order_acquire

不算難理解。 依然是讀(load)操作,它要求:當前線程所有的在load之的讀寫操作決不能被重排到load操作的前面

如下面的代碼,2和3不能被重排到1的前面,嚴格遵循load之後的讀寫操作不能被重排到load的前面。

std::atmoic<int> atm = 0;
z = atm.load(memory_order_acquire) // 1
x = 1; // 2
y = 2; // 3

幫助記憶:acquire相當於是申請了一個開門柵欄,在這個之後的所有讀寫操作不能被放到柵欄的前面。

memroy_order_release

不算難理解,與acquire合起來記憶。 這是寫(write)操作,它要求:當前線程中所有的在write之的讀寫操作決不能被重排到write操作的後面

如下面的代碼,1和2不能被重排到3的後面,嚴格遵守上面的語句。

std::atmoic<int> atm = 0;
x = 1; // 1
y = 2; // 2
atm.store(z, memory_order_release) // 3

幫助記憶:release可認爲是釋放了一個關門柵欄,在這個柵欄前的讀寫操作不能被放到柵欄的後面。

memory_order_acq_rel

它對應的是read-modify-write的操作的可見性,其他線程的寫操作(release)在本線程的modification之前可見,且本線程的modification操作之後,能夠立刻被其他線程的acquire看見。它要求:在當前線程中,所有的在此操作之前和之後的任何讀寫操作不能被重排到它的後面或者前面。

幫助記憶:相當於申請了一個嚴格的柵欄,它前面的操作只能在前面,它後面的操作只能在後面。

memory_order_seq_cst

嚴格一致性,所有的線程看到的都是完全一致的順序,這個是原子變量的默認序。

四種同步模型

Relaxed ordering

帶有memory_order_relaxed標記的原子變量,不遵循任何的同步語義,可能被任意的重排,只有原子保證。

如下, x和y初始化爲0,

// Thread 1:
r1 = atomic_load_explicit(y, memory_order_relaxed); // A
atomic_store_explicit(x, r1, memory_order_relaxed); // B
// Thread 2:
r2 = atomic_load_explicit(x, memory_order_relaxed); // C
atomic_store_explicit(y, 42, memory_order_relaxed); // D

這可能得出r1== r2 == 42的結果,也就是遵循D->A->B->C的排列。

Relaxed ordering的典型應用是counter,不關心多線程之間的內存序的同步策略(寫入之後的讀可見性),相互之間沒有任何依賴,只需要增加counter即可,例如shared_ptr的reference counter就是使用的relaxed order來增加引用計數的。那你可能也會說,那減少引用計數也是使用的relaxed order的吧?要告訴你,答案是NO。

如下是一個典型的代碼,在降低引用計數的時候,需要額外的delete ptr的操作,而且只有最後一個釋放的線程才能執行delete ptr(引用計數爲1),這就涉及到多個線程之間的內存序的同步操作:其他線程的寫操作必須要在當前線程fetch之前被看到,且在當前modification後,當前的結果要被其他線程的讀(acquire)看到。

假定使用的relaxed order,我們來分析一種情況:引用計數當前爲2,線程A嘗試減少1,線程2也嘗試減少1,線程1讀取之後發現是2,線程2讀取之後也發現是2,

class Foo {
    // ...

    std::atomic<std::size_t> refCount_{0};

    friend void intrusive_ptr_add_ref(Foo* ptr)
    {
        ptr->refCount_.fetch_add(1, std::memory_order_relaxed); // 鬆散內存序
    }

    friend void intrusive_ptr_release(Foo* ptr)
    {
        if (ptr->refCount_.fetch_sub(1, std::memory_order_acq_rel ) == 1) {  
            delete ptr;
        }
    }
};

還未寫完。。。


參考資料:

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