C++ 11 多線程初探-std::memory_order


  std::memory_order(可譯為內存序,訪存順序)

  動態內存模型可理解為存儲一致性模型,主要是從行為(behavioral)方面來看多個線程對同一個對象同時(讀寫)操作時(concurrency)所做的約束,動態內存模型理解起來稍微復雜一些,涉及了內存,Cache,CPU 各個層次的交互,尤其是在共享存儲系統中,為了保證程序執行的正確性,就需要對訪存事件施加嚴格的限制。

  假設存在兩個共享變量a, b,初始值均為 0,兩個線程運行不同的指令,如下表格所示,線程 1 設置 a 的值為 1,然后設置 R1 的值為 b,線程 2 設置 b 的值為 2,並設置 R2 的值為 a,請問在不加任何鎖或者其他同步措施的情況下,R1,R2 的最終結

果會是多少?

                          

由於沒有施加任何同步限制,兩個線程將會交織執行,但交織執行時指令不發生重排,即線程 1 中的 a = 1 始終在 R1 = b 之前執行,而線程 2 中的 b = 2 始終在 R2 = a 之前執行 ,因此可能的執行序列共有 4!/(2!*2!) = 6 種

  多線程環境下順序一致性包括兩個方面,(1). 從多個線程平行角度來看,程序最終的執行結果相當於多個線程某種交織執行的結果,(2)從單個線程內部執行順序來看,該線程中的指令是按照程序事先已規定的順序執行的(即不考慮運行時 CPU 亂序執行和 Memory Reorder)。當然,順序一致性代價太大,不利於程序的優化,現在的編譯器在編譯程序時通常將指令重新排序。對編譯器和 CPU 作出一定的約束才能合理正確地優化你的程序,那么這個約束是什么呢?答曰:內存模型。C++程序員要想寫出高性能的多線程程序必須理解內存模型,編譯器會給你的程序做優化(靜態),CPU為了提升性能也有亂序執行(動態),總之,程序在最終執行時並不會按照你之前的原始代碼順序來執行,因此內存模型是程序員、編譯器,CPU 之間的契約,遵守契約后大家就各自做優化,從而盡可能提高程序的性能。

C++11 中規定了 6 中訪存次序(Memory Order),如下:

enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
};

上述 6 中訪存次序(內存序)分為 3 類,順序一致性模型(std::memory_order_seq_cst),Acquire-Release 模型(std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel,) (獲取/釋放語義模型

)和 Relax 模型(std::memory_order_relaxed)(寬松的內存序列化模型)。

  memory_order_relaxed: 只保證當前操作的原子性,不考慮線程間的同步,其他線程可能讀到新值,也可能讀到舊值。比如 C++ shared_ptr 里的引用計數,我們只關心當前的應用數量,而不關心誰在引用誰在解引用。

  memory_order_release:(可以理解為 mutex 的 unlock 操作)

    1. 對寫入施加 release 語義(store),在代碼中這條語句前面的所有讀寫操作都無法被重排到這個操作之后,即 store-store 不能重排為 store-store, load-store 也無法重排為 store-load
    2. 當前線程內的所有寫操作,對於其他對這個原子變量進行 acquire 的線程可見
    3. 當前線程內的與這塊內存有關的所有寫操作,對於其他對這個原子變量進行 consume 的線程可見

  memory_order_acquire: (可以理解為 mutex 的 lock 操作)

    1. 對讀取施加 acquire 語義(load),在代碼中這條語句后面所有讀寫操作都無法重排到這個操作之前,即 load-store 不能重排為 store-load, load-load 也無法重排為 load-load
    2. 在這個原子變量上施加 release 語義的操作發生之后,acquire 可以保證讀到所有在 release 前發生的寫入,舉個例子:
    3. c = 0;
      thread 1:{ 
          a = 1; 
          b.store(2, memory_order_relaxed); 
          c.store(3, memory_order_release);
      }
      thread 2:{ 
          while (c.load(memory_order_acquire) != 3) ; // 以下 assert 永遠不會失敗 
          assert(a == 1 && b == 2);
           assert(b.load(memory_order_relaxed) == 2);
      }

  memory_order_consume:

    1. 對當前要讀取的內存施加 release 語義(store),在代碼中這條語句后面所有與這塊內存有關的讀寫操作都無法被重排到這個操作之前
    2. 在這個原子變量上施加 release 語義的操作發生之后,acquire 可以保證讀到所有在 release 前發生的並且與這塊內存有關的寫入,舉個例子:

      

a = 0;
c = 0;
thread 1:{
    a = 1; 
    c.store(3, memory_order_release);
}
thread 2:{ 
    while (c.load(memory_order_consume) != 3) ; 
    assert(a == 1); // assert 可能失敗也可能不失敗
}

 

memory_order_acq_rel:

    1. 對讀取和寫入施加 acquire-release 語義,無法被重排
    2. 可以看見其他線程施加 release 語義的所有寫入,同時自己的 release 結束后所有寫入對其他施加 acquire 語義的線程可見

  memory_order_seq_cst:(順序一致性)

    1. 如果是讀取就是 acquire 語義,如果是寫入就是 release 語義,如果是讀取+寫入就是 acquire-release 語義
    2. 同時會對所有使用此 memory order 的原子操作進行同步,所有線程看到的內存操作的順序都是一樣的,就像單個線程在執行所有線程的指令一樣

通常情況下,默認使用 memory_order_seq_cst,所以你如果不確定怎么這些 memory order,就用這個。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM