關於c++11 memory order的理解

ManateeFan發表於2020-08-23

關於C++memory_order的理解

看了c++併發程式設計實戰的記憶體模型部分後,一直對memory_order不太懂,今天在知乎發現了百度的brpc,恰好有關於原子操作的文件,感覺解釋的很好。為了加深理解,再次總結一遍。

在多核程式設計中,我們使用鎖來避免多個執行緒修改同一個資料時產生的競爭條件。但是,鎖會消耗系統資源,當鎖成為效能瓶頸的時候,就需要使用另一種方法——原子指令。c++11中引入了原子型別atomic

原子指令 (x均為std::atomic) 作用
x.load() 返回x的值。
x.store(n) 把x設為n,什麼都不返回。
x.exchange(n) 把x設為n,返回設定之前的值。
x.compare_exchange_strong(expected_ref, desired) 若x等於expected_ref,則設為desired,返回成功;否則把最新值寫入expected_ref,返回失敗。
x.compare_exchange_weak(expected_ref, desired) 相比compare_exchange_strong可能有spurious wakeup
x.fetch_add(n), x.fetch_sub(n) 原子地做x += n, x-= n,返回修改之前的值。

但僅靠原子指令實現不了對資源的訪問控制。這造成的原因是編譯器和cpu實施了重排指令,導致讀寫順序會發生變化,只要不存在依賴,程式碼中後面的指令可能會被放在前面,從而先執行它。cpu這麼做是為了儘量塞滿每個時鐘週期,在單位時間內儘量執行更多的指令,從而提高吞吐率。

下面看個例子:

// thread 1
// ready was initialized to false
p.init();
ready = true;
// thread 2
if(ready) {
  p.bar();
}

執行緒2在ready為true的時候會訪問p,對執行緒1來說,如果按照正常的執行順序,那麼p先被初始化,然後在將ready賦為true。但對多核的機器而言,情況可能有所變化:

  • 執行緒1中的ready = true可能會被cpu或編譯器重排到p.init()的前面,從而優先執行ready = true這條指令。線上程2中,p.bar()中的一些程式碼可能被重排到if(ready)之前。
  • 即使沒有重排,ready和p的值也會獨立地同步到執行緒2所在核心的cache,執行緒2仍然可能在看到ready為true時看到未初始化的p。

為了解決這個問題,cpu和編譯器提供了memory fence,讓使用者可以宣告訪存指令的可見性關係,c++11總結為以下memory order:

memory order 作用
memory_order_relaxed 無fencing作用,cpu和編譯器可以重排指令
memory_order_consume 後面依賴此原子變數的訪存指令勿重排至此條指令之前
memory_order_acquire 後面訪存指令勿重排至此條指令之前
memory_order_release 前面的訪存指令勿排到此條指令之後。當此條指令的結果被同步到其他核的cache中時,保證前面的指令也已經被同步。
memory_order_acq_rel acquare + release
memory_order_seq_cst acq_rel + 所有使用seq_cst的指令有嚴格的全序關係

有了memoryorder,我們可以這麼改上面的例子:

// Thread1
// ready was initialized to false
p.init();
ready.store(true, std::memory_order_release);
// Thread2
if (ready.load(std::memory_order_acquire)) {
    p.bar();
}

執行緒2中的acquire和執行緒1的release配對,確保執行緒2在看到ready==true時能看到執行緒1 release之前所有的訪存操作。

注意,memory fence不等於可見性,即使執行緒2恰好線上程1在把ready設定為true後讀取了ready也不意味著它能看到true,因為同步cache是有延時的。memory fence保證的是可見性的順序:“假如我看到了a的最新值,那麼我一定也得看到b的最新值”。

相關文章