我聽到了很多相互矛盾的答案,現在我不知道該怎么想。公認的知識是,為了在 C 中以執行緒安全的方式共享記憶體,需要將 volatile與std::mutex 一起使用。
基于這種理解,我一直在撰寫這樣的代碼:
volatile bool ready = false;
std::condition_variable cv;
std::mutex mtx;
std::unique_lock<std::mutex> lckr{ mtx };
cv.wait(lckr, [&ready]() -> bool { return ready; });
但后來我在 CppCon 上看到 Chandler Carruth 的演講,他說(作為旁注)在這種情況下不需要 volatile,我基本上不應該使用 volatile。
然后我在 Stack Overflow 中看到其他答案說永遠不應該使用 volatile,它還不夠好,而且根本不能保證原子性。
錢德勒·卡魯斯是對的嗎?我們都錯了嗎?
現在我有 3 個選項:
- 必須使用 volatile 或 std::atomic
- 任何布林值都可以
- 必須是 std::atomic
我想知道 C 14 ISO 標準是否允許我撰寫如下代碼:
#include <condition_variable>
#include <mutex>
#include <iostream>
#include <future>
#include <functional>
struct sync_t
{
std::condition_variable cv;
std::mutex mtx;
bool ready{ false };
};
static void threaded_func(sync_t& sync)
{
std::lock_guard<std::mutex> lckr{ sync.mtx };
sync.ready = true;
std::cout << "Waking up main thread" << std::endl;
sync.cv.notify_one();
}
int main()
{
sync_t sync;
{
std::unique_lock<std::mutex> lckr{ sync.mtx };
sync.ready = false;
std::future<void> thread =
std::async(std::launch::async, threaded_func, std::ref(sync));
std::cout << "Preparing to sleep" << std::endl;
sync.cv.wait(lckr, [&sync]() -> bool { return sync.ready; });
thread.get();
}
std::cout << "Done program execution" << std::endl;
return 0;
}
當我成功時會發生什么:
volatile bool ready{ false };
當我成功時會發生什么:
std::atomic<bool> ready{ false };
uj5u.com熱心網友回復:
限定符對從不同執行緒訪問物件volatile沒有必要的影響——它只保證編譯器不會優化單個執行緒中修改的副作用。來自 cppreference(粗體強調我的):
- volatile 物件- 型別為 volatile 限定的物件,或 volatile 物件的子物件,或 const-volatile 物件的可變子物件。通過 volatile 限定型別的 glvalue 運算式進行的每次訪問(讀取或寫入操作、成員函式呼叫等)都被視為用于優化目的的可見副作用(即,在單個執行執行緒中,volatile訪問不能被優化或重新排序,另一個可見的副作用是在 volatile 訪問之前或之后排序。這使得 volatile 物件適合與信號處理程式通信,但不適用于與另一個執行執行緒通信, 見 std::memory_order)。任何通過非易失性型別的左值參考易失性物件的嘗試(例如,通過對非易失性型別的參考或指標)都會導致未定義的行為。
為了防止從多個執行緒訪問物件時出現未定義的行為,您應該使用std::atomic物件。同樣,來自 cppreference:
std::atomic 模板的每個實體化和完全特化都定義了一個原子型別。如果一個執行緒寫入一個原子物件,而另一個執行緒從它讀取,則行為是明確定義的(有關資料競爭的詳細資訊,請參見記憶體模型)。
此外,對原子物件的訪問可以建立執行緒間同步并按照 std::memory_order 指定的順序對非原子記憶體訪問進行排序。
uj5u.com熱心網友回復:
不, volatile 是令人困惑的關鍵字,但它與并發無關,不像在 C# 或 Java 中它保證順序一致性。這里只是提示編譯器不要優化變數。
uj5u.com熱心網友回復:
volatile 只是告訴編譯器即使你不知道是誰也可能改變這個值,例如,它可能是一些硬體、信號,甚至是其他執行緒。一個著名的例子是:
bool flag
foo()
{
flag = true;
while(flag)
{
}
}
優化的編譯器將看到 flag 為真,并且由于它只是一個普通的全域變數,它可以假設除了當前執行緒之外沒有人可以更改它,因此編譯器可能會假設 flag 始終為真,因此切換while(flag)到while(1)以使無限環形。但是如果你把flag變數宣告為volatile,編譯器就不能假設只有當前執行緒接觸到這個值,所以代碼會保持不變。
現在對于您的問題, volatile 將幫助我們通知編譯器其他人可能會使用此值,但是對于多執行緒來說它還不夠,因為它不能防止資料競爭,這是 c 語言中未定義的行為,因此我們需要將 bool 標志宣告為 std::atomic。
請注意,編譯器從 std::atomic 宣告中理解的一件事是另一個執行緒可能會使用此值,因此我們無法進行上述優化。
對于您的示例,正??如我們所解釋的那樣, volatile 是不夠的,但是您也不需要 std::atomic ,因為您有鎖,因此如果您的鎖正常作業,那么當您在關鍵部分內時,沒有其他執行緒可能會觸及該值所以 std::atomic 是多余的。
std::atomic 主要用于臨界區,當所有臨界區都是因為原子操作時,所以我們可以使用 std::atomic 代替更慢的鎖(并非總是如此,這取決于流程)。
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/423033.html
標籤:
