我仍在學習涉及網路的多執行緒編程。
我的問題是,當我將執行緒設計為消費者/生產者模式,并且生產者隨機修改變數時,消費者檢查變數并基于它做一些事情,它是否仍然很糟糕(大問題)?
就像下面的代碼。
int flag = 0;
void producer()
{
while(true)
{
// waits until packet arrives.
recv(socket, data);
if(data == 1)
flag = 1;
}
}
void consumer()
{
while(true)
{
if(flag == 1)
doSomething();
}
}
我猜doSomething()函式最終會被呼叫。我不需要完美地同步這兩個執行緒。所以我認為即使我不使用 likestd::atomic_int代替int. (我會std::atomic_int在這個例子中使用,但在我的實際實作中,該變數太復雜而無法使用std::atomic。)
那么忽略資料競爭問題是不好的做法嗎?或者它是性能的不錯選擇?
uj5u.com熱心網友回復:
資料競爭導致未定義的行為。
C 草案 N4860
6.9.2.1 資料競賽 (21.2)
如果程式的執行包含兩個潛在的并發沖突操作,則程式的執行包含資料競爭,其中至少一個不是原子的,并且兩者都不會在另一個之前發生,除了下面描述的信號處理程式的特殊情況。任何此類資料競爭都會導致未定義的行為。
和
3.30 未定義的行為
允許的未定義行為包括完全忽略情況并產生不可預測的結果,[...]
因此,如果您可以忍受未定義的行為,例如格式化的硬碟或鼻惡魔,那是您的選擇。
如果它是一個 Hello world 程式,您可能會僥幸逃脫。如果是超過 2000 歐元的銀行交易丟失,我認為這是一個問題。在我的業務中,如果我不小心錯過了帶有測量點的網路包,飛機可能會墜毀。我認為這是一個大問題。
在你寫的評論中:
實際上我正在做的是游戲開發,有時我需要選擇性能而不是正確性。
誠然,在游戲開發中,您會在性能和正確性之間做出妥協。但通常這意味著正確性的數量是可測量或可估計的。例如,物理引擎每秒只更新 50 次而不是每秒 100 次。但是,結果在可能的范圍內是正確的。
對于未定義的行為,很難說出“不可預測的結果”實際上有多不正確。例如,您不希望結果隨著每個新編譯器而改變。
我建議你以執行緒安全的方式實作它,然后檢查你是否真的有性能問題。我敢打賭:會有一個不需要未定義行為并且仍然足夠快的解決方案。以前已經做過了。
過早的優化是萬惡之源。
正如唐納德·克努斯所說。
uj5u.com熱心網友回復:
拋開所有的理論不談,這個非常簡單的例子在現實生活中已經完全被打破了。
因為資料競爭導致未定義的行為,編譯器有權假設非原子變數不會異步更改。因此,優化編譯器可以并將您consumer轉換為:
void consumer()
{
while (true) {
if (flag == 0) {
while (true); // infinite loop
} else {
doSomething();
}
}
}
也就是說,如果在任何迭代中flag等于 0,編譯器可以看到消費者執行緒flag在再次檢查之前不會寫入或呼叫任何函式。因此,它假定flag在再次檢查之前不會改變。因此,它得出結論,我們已經知道在下一次檢查時它仍然是錯誤的。啊哈,優化!我們可以節省下一次實際測驗標志的費用,并且永遠保持回圈,因為沒有什么可以讓我們崩潰。
您可以看到g -O3正是這樣做的:https ://godbolt.org/z/414PG4nnE 。請注意匯編輸出的第 13 行和第 21 行的無限回圈。(它將這兩種情況分為flag最初是否為假或在以后的迭代中是否為假。)
如果flag最初為真,它將呼叫doSomething(),在這種情況下,它將再次測驗該標志,以防doSomething()自己可能已更改它。但是,如果編譯器能夠(通過行內、鏈接時優化等)判斷doSomething()不寫入flag,它也可以優化重新測驗,并在第一次呼叫后無條件地進入無限回圈。
實際上,您可以flag通過創建volatile變數來使其作業。然而,(1)這在理論上仍然不能解決資料競爭,(2)它會施加幾乎所有相同的優化懲罰std::atomic。因此,您不妨在使用std::atomic它的同時使用并獲得正確性。
轉載請註明出處,本文鏈接:https://www.uj5u.com/gongcheng/447422.html
上一篇:Java中的原子性是什么
