C++ 中的 volatile 和 atomic
0. TL;DR
-
std::atomic 用於多執行緒併發場景,有兩個典型使用場景:
- 原子操作:對 atomic 變數的操作(讀/寫/自增/自減)彷彿受互斥量保護。一般透過特殊的機器指令實現,比使用互斥量更高效
- 限制編譯器/硬體對賦值操作的重新排序
-
volatile 和多執行緒併發沒有任何關係,用於防止編譯器最佳化掉對特殊變數的“冗餘”讀寫操作
1. Data Race 和未定義行為
C++ 中有很多未定義行為,Data Race 便是其中之一。
⚠️ 如果一個變數不是 atomic 變數,且沒有互斥量保護,在一個執行緒中執行寫操作,同時在另一個執行緒中讀取,則會產生 Data Race,其行為未定義!
例如在執行下面程式碼的過程中,另一個執行緒同時讀取 i 的值,讀到的值可能是 -13,42,0,43923... 任何值!
int i = 0;
++i;
--i;
雖然在這種場景下,你讀到的大機率是0/1,但要知道,對於未定義行為,理論上可能讀到任何值!!
2. atomic
std::atomic 是 C++ 中的模版類,一般用於 bool、整型、指標型別,如 atomic<bool>
,atomic<int>
,atomic<Widget*>
等。對 atomic 變數的操作(讀/寫/自增/自減)彷彿受互斥量保護(底層一般透過特殊的機器指令實現,比使用互斥量更高效)。
2.1 原子操作
atomic 的第一個應用場景就是多執行緒讀寫變數:
atomic<int> ai {0};
ai = 10; // 原子寫
std::cout << ai; // 原子讀
++ai; // 原子自增為11
--ai; // 原子自減為10
執行上述程式碼期間,如果在另一個執行緒讀取 ai 的值,只可能讀到 0/10/11,不可能有其他結果。
2.2 限制重排序
atomic 的第二個應用場景是,當某個變數在兩個任務之間傳遞資訊時,防止對該變數賦值進行重新排序。假設 a、b、x、y 是 4 個獨立的變數:
a = b;
x = y;
為了提升效能,編譯器可能會對不相關的賦值進行重新排序為:
x = y;
a = b;
即使編譯器不這麼做,底層硬體也可能這麼做。atomic 可以避免這種重排序。例如在一個任務中計算 value,另一個任務中依賴 value 的可用性,則可以藉助 atomic 變數實現:
atomic<bool> valueAvailable(false);
auto value = computeValue();
valueAvailable = true;
std::atomic 預設採用順序一致性模型,會限制重新排序:不僅編譯器會保證 value 在 valueAvailable 之前賦值,底層硬體也保證這個順序。
⚠️ C++ 還提供了其他更靈活的一致性模型(如 memory_order_relaxed),除非你是這方面的專家,很清楚不同記憶體序產生的影響,否則不要輕易使用。
2.3 load/store
有的開發者喜歡使用 load()/store() 成員函式,這不是必須,但可以起到強調作用:
- 強調該變數涉及多執行緒併發操作
- 強調 atomic 變數可能導致效能問題
- 雖然 atomic 比互斥量更高效,但仍然比普通變數慢、開銷大
- atomic 變數可能會阻止重新排序最佳化
3. volatile
如果將上面的例子中的 atomic<bool>
換成 volatile bool
,既無法保證操作的原子性,也無法限制對 value 和 valueAvailable 賦值的重新排序。
volatile bool valueAvailable(false);
auto value = computeValue();
valueAvailable = true;
對於普通記憶體來說,如果向某個記憶體寫一個值,該值會一直保留在記憶體,直到被下一個寫操作覆蓋。編譯器可以對普通記憶體的變數讀寫進行最佳化,例如下面這段程式碼(雖然一般開發者不會直接寫出這樣的程式碼,但是經過模版例項化、內聯、重排序等最佳化後,很可能產生類似的程式碼):
int x;
auto y = x; // 讀取 x
y = x; // 再次讀取 x
x = 10; // 寫入 x
x = 20; // 再次寫入 x
可能被最佳化為:
auto y = x;
x = 20;
但是在有些特殊場景下(例如和外設互動),變數對應特殊的記憶體區域:
-
x 可能是一個感測器的值,連續兩次讀取 x 的值可能不同,不應該被最佳化
-
x 可能對應某個無線電發射器的控制埠,
x = 10
和x = 20
對應兩條不同的指令,不應該被最佳化
volatile 的作用正是告訴編譯器,某個變數所對應記憶體是特殊記憶體,不要進行任何最佳化!
將 x 宣告為 volatile int x
即可避免上述最佳化,而宣告為 atomic<int> x
則沒有這個效果。
⚠️ 經常聽到一種說法:開啟編譯器最佳化選項之後,會導致程式行為異常。但通常都不是編譯器的問題,而是自己的程式碼不規範,或依賴了未定義行為導致的!
思考:
- volatile 和 atomic 可以同時使用嗎,用於什麼場景?
- volatile 和 const 可以同時使用嗎,用於什麼場景?
4. Reference
《Effective Morden C++》條款 40