C語言原子操作的應用(記憶體次序,記憶體屏障)

2020-07-16 10:04:30

記憶體次序

為優化程式程式碼,編譯器和處理器可以自由地對任何無相互依賴關係的命令進行重新排列。例如,兩個分配語句 a=0;B=1;,它們可以以任一順序執行。然而,在多執行緒環境下,由於不同執行緒記憶體操作之間的依賴性對於編譯器或處理器通常是不可見的,所以對編譯器或處理器執行命令重新排序可能會引發錯誤。

使用原子物件可以預設地防止此類重新排序。但是,防止優化意味著可能會犧牲速度。有經驗的程式設計師可以在較低的記憶體次序請求下,通過明確地使用原子操作提高效能。

對於每個執行原子操作的函數(例如 atomic_store()),都有另一個版本,這些函數的名稱以 _explicit 結尾,如 atomic_store_explicit(),它們增加了一個型別為 memory_order 的引數。

memory_order 型別是一個列舉,它定義了以下常數,以指定特定的記憶體次序請求:

memory_order_relaxed

呼叫者指定無任何記憶體次序請求,從而使編譯器可以自由地改變操作的順序。

memory_order_release

指定在當前執行緒 T1 中對一個原子物件A進行寫存取時執行釋放操作(release operation)。釋放操作的作用是:當另一個執行緒 T2 對 A 執行捕獲操作時(讀存取),所有 T1 曾對 A 執行的操作在 T2 捕獲 A 以後,對 T2 都是可見的。

memory_order_acquire

指定對一個原子物件進行讀存取時執行捕獲操作(acquire operation)。它確保在該函數呼叫前,後續的記憶體存取操作不發生重新排列。

memory_order_consume

一個消耗操作(consume operation)的限制小於一個捕獲操作:它僅當後續記憶體存取操作直接依賴讀取原子變數時,防止重新排序。

memory_order_acq_rel

同時具有捕獲和釋放操作。

memory_order_seq_cst

順序一致性(sequential consistency)請求包括對 memory_order_acq_rel 的捕獲和釋放操作。此外,它還指定了所有操作按一個次序嚴格執行,該次序為所包含原子物件的修改次序。順序一致性是預設的記憶體順序請求,如果沒有顯式指定更低的請求,這種請求會應用到所有原子操作。

如果將 counter 宣告為原子物件,自增和自減 counter 都是獨立於其他操作的,因此不必指定記憶體存取次序。換句話說,在下述語句位置:
++counter;            // 隱含memory_order_seq_cst

下面語句充分且允許編譯器執行更多的優化:
atomic_fetch_add_explicit( &counter, 1, memory_order_relaxed );

釋放和捕獲操作是在命令間建立 happens-before 關係的有效途徑。換句話說,如下例所示,_explicit 函數確保一個執行緒完成操作 A 後才能執行操作 B:
struct Data *dp = NULL, data;
atomic_intptr_t aptr = ATOMIC_VAR_INIT(0);

// 執行緒1
   data = ...;                                          // 操作A
   atomic_store_explicit( &aptr, (intptr_t)&data,
                          memory_order_release );

// 執行緒2
   dp = (struct Data*)atomic_load_explicit( &aptr,
                                            memory_order_acquire );
   if( dp != NULL)
      // 處理*dp所參照的資料
                                                            // 操作B
   else
      // *dp所參照的資料還不可獲得

對於一個使用互斥同步的程式,當互斥鎖定時,隱含了一個捕獲操作,當互斥解鎖時,隱含了一個釋放操作。這意味著:如果一個執行緒 T1 使用互斥來保護一個操作 A,而另一個執行緒 T2 使用相同的互斥來保護一個操作 B,假如 T1 先鎖定互斥,那麼操作 A 將先完成執行,然後才執行操作 B。相反,假如 T2 先鎖定互斥,那麼當 T1 執行操作 A 時,通過操作 B 所執行的所有修改,對於執行緒 T1 是可見的。

柵欄(記憶體屏障)

對於一個原子操作的記憶體次序請求,也可以通過一個原子操作單獨指定。這種技術被稱為建立一個柵欄(fence)記憶體屏障(memory barrier)。要設定一個柵欄,C11 提供了以下函數:
void atomic_thread_fence(memory_order order);

若引數值為 memory_order_release,函數 atomic_thread_fence()建立一個釋放柵欄(releas fence)。在這種情況下,原子寫操作必須在釋放柵欄之後發生

若引數值為 memory_order_acquire 或 memory_order_consume,函數 atomic_thread_fence()建立一個捕獲柵欄(acquire fence)。在這種情況下,原子讀操作必須在捕獲柵欄之前發生

柵欄允許更大程度的記憶體順序優化。
// 執行緒2
   dp = (struct Data*)atomic_load_explicit( &aptr, memory_order_relaxed );
   if( dp != NULL)
   {
      atomic_thread_fence(memory_order_acquire);
      // 操作B:處理*dp所參照的資料
   }
   else
      // *dp所參照的資料還不可獲得