理解 C++ 的 Memory Order 以及 atomic 与并发程序的关系
為什么需要 Memory Order
如果不使用任何同步機制(例如 mutex 或 atomic),在多線程中讀寫同一個變量,那么,程序的結(jié)果是難以預(yù)料的。簡單來說,編譯器以及 CPU 的一些行為,會影響到程序的執(zhí)行結(jié)果:
- 即使是簡單的語句,C++ 也不保證是原子操作。
- CPU 可能會調(diào)整指令的執(zhí)行順序。
- 在 CPU cache 的影響下,一個 CPU 執(zhí)行了某個指令,不會立即被其它 CPU 看見。
原子操作說的是,一個操作的狀態(tài)要么就是未執(zhí)行,要么就是已完成,不會看見中間狀態(tài)。例如,在 C++11 中,下面程序的結(jié)果是未定義的:
int64_t i = 0; // global variableThread-1: Thread-2:i = 100; std::cout << i;C++ 并不保證i = 100是原子操作,因為在某些 CPU Architecture 中,寫入int64_t需要兩個 CPU 指令,所以 Thread-2 可能會讀取到i在賦值過程的中間狀態(tài)。
另一方面,為了優(yōu)化程序的執(zhí)行性能,CPU 可能會調(diào)整指令的執(zhí)行順序。為闡述這一點,下面的例子中,讓我們假設(shè)所有操作都是原子操作:
int x = 0; // global variableint y = 0; // global variableThread-1: Thread-2:x = 100; while (y != 200)y = 200; ;std::cout << x;如果 CPU 沒有亂序執(zhí)行指令,那么 Thread-2 將輸出100。然而,對于 Thread-1 來說,x = 100;和y = 200;這兩個語句之間沒有依賴關(guān)系,因此,Thread-1 允許調(diào)整語句的執(zhí)行順序:
Thread-1:y = 200;x = 100;在這種情況下,Thread-2 將輸出0或100。
CPU cache 也會影響到程序的行為。下面的例子中,假設(shè)從時間上來講,A 操作先于 B 操作發(fā)生:
int x = 0; // global variableThread-1: Thread-2:x = 100; // A std::cout << x; // B盡管從時間上來講,A 先于 B,但 CPU cache 的影響下,Thread-2?不能保證立即看到 A 操作的結(jié)果,所以 Thread-2 可能輸出0或100。
  從上面的三個例子可以看到,多線程讀寫同一變量需要使用同步機制,最常見的同步機制就是std::mutex和std::atomic。然而,從性能角度看,通常使用std::atomic會獲得更好的性能。
   C++11 為std::atomic提供了 4 種 memory ordering:
- Relaxed ordering
- Release-Acquire ordering
- Release-Consume ordering
- Sequentially-consistent ordering
默認情況下,std::atomic使用的是 Sequentially-consistent ordering。但在某些場景下,合理使用其它三種 ordering,可以讓編譯器優(yōu)化生成的代碼,從而提高性能。
Relaxed ordering
  在這種模型下,std::atomic的load()和store()都要帶上memory_order_relaxed參數(shù)。Relaxed ordering 僅僅保證load()和store()是原子操作,除此之外,不提供任何跨線程的同步。
   先看看一個簡單的例子:
| 1 2 3 4 5 6 | std::atomic<int> x = 0; // global variable std::atomic<int> y = 0; // global variable Thread-1: ????????????????????????????????????????????????????????????????Thread-2: r1 = y.load(memory_order_relaxed); // A ????????????????r2 = x.load(memory_order_relaxed); // C x.store(r1, memory_order_relaxed); // B ????????????????y.store(42, memory_order_relaxed); // D | 
執(zhí)行完上面的程序,可能出現(xiàn)r1 == r2 == 42。理解這一點并不難,因為編譯器允許調(diào)整 C 和 D 的執(zhí)行順序。如果程序的執(zhí)行順序是 D -> A -> B -> C,那么就會出現(xiàn)r1 == r2 == 42。
如果某個操作只要求是原子操作,除此之外,不需要其它同步的保障,就可以使用 Relaxed ordering。程序計數(shù)器是一種典型的應(yīng)用場景:
#include <cassert> #include <vector> #include <iostream> #include <thread> #include <atomic> std::atomic<int> cnt = {0}; void f() {for (int n = 0; n < 1000; ++n) {cnt.fetch_add(1, std::memory_order_relaxed);} } int main() {std::vector<std::thread> v;for (int n = 0; n < 10; ++n) {v.emplace_back(f);}for (auto& t : v) {t.join();}assert(cnt == 10000); // never failedreturn 0; }Release-Acquire ordering
在這種模型下,store()使用memory_order_release,而load()使用memory_order_acquire。這種模型有兩種效果,第一種是可以限制 CPU 指令的重排:
- 在store()之前的所有讀寫操作,不允許被移動到這個store()的后面。
- 在load()之后的所有讀寫操作,不允許被移動到這個load()的前面。
  除此之外,還有另一種效果:假設(shè) Thread-1?store()的那個值,成功被 Thread-2?load()到了,那么 Thread-1 在store()之前對內(nèi)存的所有寫入操作,此時對 Thread-2 來說,都是可見的。
   下面的例子闡述了這種模型的原理:
讓我們分析一下這個過程:
- 首先 A 不允許被移動到 B 的后面。
- 同樣 D 也不允許被移動到 C 的前面。
- 當 C 從 while 循環(huán)中退出了,說明 C 讀取到了 B?store()的那個值,此時,Thread-2 保證能夠看見 Thread-1 執(zhí)行 B 之前的所有寫入操作(也即是 A)。
如下例子展示了在三個線程之間進行 release-acquire 內(nèi)存序的同步機制實例:
- 其中對于讀取修改在寫入的操作,可以使用?memory_order_acq_rel 標志位,如下所示。
Release-Consume ordering
比起 Release-Acquire 模型較為內(nèi)存限制較弱,僅對 load 該原子變量具有依賴的相關(guān)變量有效。例子如下所示:
#include <thread> #include <atomic> #include <cassert> #include <string>std::atomic<std::string*> ptr; int data;void producer() {std::string* p = new std::string("Hello");data = 42;ptr.store(p, std::memory_order_release); }void consumer() {std::string* p2;while (!(p2 = ptr.load(std::memory_order_consume)));assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptrassert(data == 42); // may or may not fire: data does not carry dependency from ptr }int main() {std::thread t1(producer);std::thread t2(consumer);t1.join(); t2.join(); }與?volatile 關(guān)鍵字的區(qū)別
- std::atomic用于無鎖條件下,實現(xiàn)多線程間的數(shù)據(jù)訪問,是編寫并發(fā)程序的有效工具之一
- volatile用于讀寫內(nèi)存時禁止編譯器進行內(nèi)存優(yōu)化,是用于專有內(nèi)存的有效工具之一
參考資料
- C++ atomics and memory ordering
- cppreference.com - std::memory_order
- Atomic Usage examples
- C++11 introduced a standardized memory model. What does it mean?
- bRPC - Memory fence
- Acquire and Release Semantics
總結(jié)
以上是生活随笔為你收集整理的理解 C++ 的 Memory Order 以及 atomic 与并发程序的关系的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: CMake 常用命令和变量
- 下一篇: 【Java】位运算判断2的N次幂
