簡體   English   中英

是否安全地解除引用不同線程中原子對象的READ ONLY非原子指針?

[英]Is dereferencing a READ ONLY non-atomic pointer to an atomic object in different threads safe?

如果我寫這樣的東西:

std::atomic<bool> *p = new std::atomic<bool>(false); // At the beginning of the program  

//...  

void thread1()  
{  
    while (!(*p))  
        // Do something  
}  

//...  

void thread2()  
{  
    //...  
    *p = true;  
    //...
}  

thread1thread2將同時運行。 p的值從未初始化,因此永遠不會改變。 在這種情況下,取消引用操作是否安全? 出於性能原因,我想避免使用原子指針。

是的,這是安全的。 如果沒有至少一個線程修改共享變量,則無法進行數據競爭。 由於兩個線程都沒有修改p ,所以沒有比賽。

您發布的代碼和問題是兩個不同的事情。

代碼將起作用,因為您取消引用非原子指針。 您取消引用std::atomic<bool>*將導致(運算符重載)順序一致的提取/存儲。 這可能不是必要的效率(大多數時候這樣的標志用於釋放操作),但它是安全的。

否則,只要沒有其他線程修改數據,解除引用任何 (包括原子變量)的有效非原子指針是安全的。

取消引用非原子指針與寫入它的另一個線程仍然是“安全的”,因為它不會崩潰。 然而,沒有正式保證記憶沒有亂碼(對於對齊的POD,由於處理器如何訪問存儲器而有非常實用的保證​​),但更重要的是,在沒有存儲器排序保證的情況下它是不安全的 使用這樣的標志時,通常會做這樣的事情:

do_work(&buf); // writes data to buf
done = true;   // synchronize

這適用於一個線程,但不保證在並發時正常工作。 為此,您需要事先保證。 否則,在實現對數據的寫入之前,另一個線程可能會獲取對標志的更新。

解除引用(即:讀取地址) 在intel架構上是原子的。 此外,由於不變,我猜它不僅在Intel / AMD上是正確的。 不過請看這篇文章了解更多信息。

澄清:在其他體系結構中,當寫入地址時,如果只修改了部分地址,則可以切換線程,因此其他線程讀取的地址將無效。

對於Intel, 如果地址在內存中對齊,則不會發生這種情況。

此外,由於*pstd::atomic<bool> ,它已經實現了所有需要的東西(native,asm,memory fences)。

這取決於你的兩次訪問是什么。 如果主設備在設置布爾值之前寫入一些數據,則從設備需要一個內存屏障以確保它不會在布爾值之前讀取所述數據。

也許現在你的線程只是等待這個布爾值退出,但是如果有一天你決定主機應該,例如,將終止狀態傳遞給從機,你的代碼可能會中斷。
如果你在6個月后回來並修改這段代碼,你確定你會記得你的從屬循環之外的區域是一個無共享讀區域,而你掌握布爾之前的區域是一個非共享寫區域?

無論如何,你的布爾值需要是易變的,否則編譯器可能會優化它。 或者更糟糕的是,你的同事的編譯器可能會,而你將不再存在另一段不可靠的代碼。

眾所周知,volatile變量通常不足以進行線程同步,因為它們沒有實現內存障礙,如下例所示:

主人:

// previous value of x = 123
x = 42;
*p = true;

從處理器上的總線邏輯:

write *p = true

奴隸:

while (!*p) { /* whatever */ }
the_answer = x; // <-- boom ! the_answer = 123

從機處理器上的總線邏輯:

write x = 42 // too late...

(如果主站的總線寫入無序排列,則會出現對稱問題)

當然,您可能永遠不會在您的特定台式計算機上看到如此罕見的事件,就像您可能偶然運行一個程序破壞其自身的內存而不會崩潰。

然而,使用這種泄漏同步編寫的軟件正在勾選時間炸彈。 在一系列總線架構上編譯並運行它們,並且有一天...... Ka-boom!


作為事實上,C ++ 11被允許創建一樣,如果有什么給它的任務傷害多處理器編程了很多 ,並在同一時間提供什么,但糟糕的原子能,互斥體和條件變量處理同步(與當然是血腥尷尬的未來。

同步任務(尤其是工作線程)的最簡單和最有效的方法是讓它們在隊列上處理消息。 這就是驅動程序和實時軟件的工作方式,除非出現一些非凡的性能要求,否則任何多處理器應用程序都應該如此。

強制程序員用美化的標志來控制多任務是很愚蠢的 您需要非常清楚地了解硬件如何與原子計數器一起使用。
C ++的迂腐集團再次迫使每個人和他的狗成為另一個領域的專家,只是為了避免編寫糟糕,不可靠的代碼。

像往常一樣,你會讓大師們用一種放縱的微笑噴出他們的“良好做法”,而人們在破碎的自制隊列中愚蠢的旋轉循環中燃燒兆焦耳的CPU能力,相信“無等待”同步是阿爾法和歐米茄效率。

而這種表現迷戀是一個非問題。 “阻塞”調用只消耗可用計算能力的碎屑,而且還有許多其他因素會使性能比操作系統同步原語高出幾個數量級(缺少標准方法來定位任務)給定處理器,一開始)。

考慮你的thread1奴隸。 訪問一個原子bool將把一把沙子扔進公交緩存齒輪,使這個特殊訪問速度減慢大約20倍。浪費了幾十個周期。 除非你的奴隸只是在循環中弄亂它的虛擬拇指,否則這幾個循環將相形見絀,單個循環將持續數千或數百萬。 此外,如果你的奴隸完成工作而其兄弟奴隸不在,會發生什么? 它會在這個標志上無用地旋轉並浪費CPU,還是阻塞任何互斥鎖?
這正是為了解決消息隊列被發明的這些問題。

像消息隊列讀取這樣的適當OS調用可能會消耗幾百個周期。 所以呢?
如果您的從屬線程只是增加3個計數器,那么您的設計是錯誤的。 你沒有啟動一個線程來移動幾個火柴棍,就像你沒有為每個字節分配你的內存字節一樣,即使是像C ++這樣的高級語言。

如果您不使用線程來修復面包屑,您應該依賴簡單且經過驗證的機制,例如等待隊列或信號量或事件(因缺少便攜式解決方案而選擇posix或Microsot),並且您不會注意到對性能的任何影響任何。

編輯 :更多關於系統調用開銷

基本上,對等待隊列的調用將花費幾微秒。

假設您的普通工作人員數量為10到100毫秒,系統調用開銷將無法從背景噪聲中獲得,並且線程終止響應將保持在可接受的限制范圍內(<0.1 s)。

我最近實現了一個Mandelbrot set explorer作為並行處理的測試用例。 它絕不代表所有並行處理案例,但我仍然注意到一些有趣的事情。

在我的I3 Intel 2核/ 4個CPU @ 3.1 GHz上,每個CPU使用一個工作器,我測量了純計算並行化的增益因子(即使用1個核心超過4個核心的執行時間的比率)(即沒有任何數據依賴性)工人之間)。

  • 在每個核心上本地化線程(而不是讓OS調度程序將線程從一個核心移動到另一個核心)將比率從3.2提高到3.5(理論上最大值為4)

  • 除了將線程鎖定到不同的核心之外,最值得注意的改進是由於算法本身的優化(更有效的計算和更好的負載平衡)。

  • 用於讓4名工作人員從公共隊列中抽取的大約1000個C ++ 11互斥鎖的成本達到7毫秒,即每次調用7微秒。

我很難想象高性能設計每秒執行超過1000次同步(或者你的時間可能更好地用於改進設計),所以基本上你的“阻塞”調用成本遠低於1%的可用功率。相當低廉的PC。
選擇是你的,但我不確定從一開始就實施原始原子對象將是性能的決定性因素。

我建議從簡單的隊列開始並做一些基准測試。 您可以使用pthread posix接口,或者將此非常好的示例作為轉換為C ++ 11的基礎。

然后,您可以調試程序並在同步無錯誤的環境中評估算法的性能。

如果隊列被證明是真正的CPU占用並且您的算法無法重構以避免過多的同步調用,那么切換到您認為更高效的任何自旋鎖應該相對容易,特別是如果您的計算已經簡化並且數據依賴性已經排序事先出去。

PS :如果這不是商業秘密,我很樂意聽到更多關於你的算法的消息。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM