2012-04-03 58 views
2

我有一個關於比賽條件和同時寫道的問題。比賽條件和解鎖寫

我有一個類,誰的對象是從不同的線程訪問。我想僅根據需要計算一些值並緩存結果。出於性能原因,我寧願不使用鎖(在任何人詢問之前 - 是的,在我的情況下是相關的)。

這構成了競爭條件。但是,這些對象是const的,不會被改變。所以如果不同的線程計算要緩存的值,它們在我的用例中保證是相同的。在不鎖定的情況下編寫這些值是否安全?或者,從更廣泛的角度來看,將不同鎖定的內容從不同線程寫入內存是否安全?

寫入的值是bool和double類型,有問題的架構可能是x86和ARM。

編輯:感謝大家的輸入。我終於決定找到一種不涉及緩存的方式。這種方法看起來很像'黑客',並且存在使用標誌變量的問題。

+0

我懷疑C++會保證它,但至少在現代硬件上,很難想象會出現什麼問題。 – 2012-04-03 17:27:30

+0

對原語的每個寫入操作都是原子操作,所以如果許多線程同時寫入相同的一組字段,它應該沒有區別。 – Alain 2012-04-03 17:28:22

+1

「因此,如果不同的線程計算要緩存的值,它們在我的用例中保證是相同的」......這很奇怪。如果你能保證所寫的值是相同的,爲什麼要同時寫入它們?或者更好的是,爲什麼不止一次寫入它們? – 2012-04-03 17:36:58

回答

4

正如你所說,這是一種競爭條件。在C++ 11下,它在技術上是數據競賽和未定義的行爲。沒有關係,值是相同的。

如果你的編譯器支持它(比如最近的GCC或GCC或MSVC我Just::Thread庫),那麼你可以使用std::atomic<some_pod_struct>,以提供圍繞你的數據的原子包裝(假設它一個POD結構---如果它不是那麼你有更大的問題)。如果它足夠小,那麼編譯器將使它無鎖,並使用適當的原子操作。對於較大的結構,圖書館將使用鎖。

這樣做沒有原子操作或鎖的問題是知名度。雖然在x86或ARM上的處理器級別沒有問題,但是從多線程/處理器到同一內存寫入相同的數據(假設它實際上是字節 - 字節相同),考慮到這是一個緩存,我期望你會想要讀取這些數據,而不是重新計算它,如果它已經被寫入。因此,你需要某種標誌來表明完成。除非您使用原子操作,鎖定或合適的內存屏障指令,否則在數據執行之前,「準備就緒」標誌可能對另一個處理器可見。這會讓事情變得很糟糕,因爲第二個處理器現在讀取的是一組不完整的數據。

您可以使用非原子操作寫入數據,然後使用原子數據類型作爲標誌。在C++ 11下,這將產生合適的內存屏障和同步,以確保數據對於任何看到該標誌集的線程都可見。對於兩個線程來說,寫入數據仍然是未定義的行爲,但實際上它可能還行。

或者,將數據存儲在由執行計算的每個線程分配的堆內存塊中,並使用比較和交換操作來設置原子指針變量。如果比較和交換失敗,那麼另一個線程首先到達那裏,因此釋放數據。

+0

被選爲用標記變量指出問題和需要內存屏障的最佳答案。 – 2012-04-04 13:06:46

+0

@Anthony,C++數據競賽的技術含義與競爭條件的一般概念+1。 C++只規定了什麼是數據競爭和UB。因此,如果共享變量是C++ 11原子類型,那麼即使標記爲w/memory_order_relaxed排序,它也能保證數據競爭免費。該標準沒有提到一般的競賽條件概念,並沒有說在發生這種情況時存在UB。它完全依賴程序員添加propery同步原語來確保正確的語義。你同意嗎? – 2014-08-29 02:50:42

+0

@Anthony,還有一些人認爲C++ 11中仍然存在數據競爭,即使共享數據是具有輕鬆排序的原子類型。我很難嘗試向他們解釋這些概念(在語言和書中正確定義)。您可以看看[這篇文章](https://groups.google.com/forum/#!topic/comp.lang.c++.moderated/1SN85LRbouw)並與我們分享您的意見嗎?謝謝。 – 2014-08-29 02:54:37

1

最終答案可能取決於您的數據結構。

在「非便攜式」領域,您可能需要查看compare and swap,大多數處理器會讓您在指針大小的實體上進行操作。要訪問您可以使用內聯彙編(在x86上,這些是lock cmpxchg指令),或者可能是GCC同步擴展。在看到一個未初始化的值時,每個線程都可以急切地進行初始化,併發出一個比較和交換來嘗試設置一個值。如果比較和交換失敗,則意味着另一個線程擊敗了它。

最終該操作的使用往往最終被實現自旋鎖,不過,你可能會尋找避免相當於...

+0

部分數據結構將是(32位)ARM上的8字節雙精度,因此它們大於'指針大小的實體'。所以這不是一個選擇。 – 2012-04-03 17:39:31

+0

@GabrielSchreiber - 這並不意味着它不是一個選項。例如,你可以初始化一個指向結構的指針,而不是結構本身。或者你可以有一個「伴侶」這個詞,它基本上作爲初始化的一個鎖定 - 來確定它是否已經被初始化過。但是,一般來說,如果他們一次只寫多個單詞,就會鼓勵你小心地把事情稱爲「原子」。 – asveikau 2012-04-03 17:42:50

+0

@GabrielSchreiber - 我必須強調,如果你想讀取 - 修改 - 寫入操作是原子性的,比較和交換(或特定平臺的等價物;「load-link/store-conditional」是RISC版本)是非常漂亮的唯一的辦法就是走。 – asveikau 2012-04-03 17:54:58

1

你並不需要防止來自不同線程寫入POD變量,如果這些值將是相同的。但是,如果你有指針,你肯定應該做一個互鎖交換。

更新:爲了澄清您的情況,緩存和優化不會產生任何不利影響,因爲您正在所有線程上寫入完全相同的值。出於同樣的原因,您不需要製作變量volatile。唯一可能成爲問題的是如果變量未與機器的字大小對齊。有關更多詳細信息,請參閱https://stackoverflow.com/a/54242/677131。默認情況下,變量會自動對齊,但您可以明確更改對齊方式。

還有一種方法可以完全避免這個問題。由於變量將具有相同的值,可以在併發執行開始前預先計算它們的值,也可以讓每個線程擁有自己的副本。後者具有在NUMA機器上提供更好性能的優勢。

1

我必須說,使用鎖通常是這樣做的寫從多個線程相同的變量,即使數據比處理器字大不能是不安全的正確方法,但...

啓動尺寸。沒有過渡狀態,因爲至少有一個線程會完成寫入值,所以變量可能會被損壞。其他線程不會通過扭轉相同的值來改變它。

所以,如果有保證計算結果永遠是相同的,不管什麼線程,多線程沒有危險。在計算之前只需檢查一個標誌(「已經計算好?」)。多個線程將進入價值計算代碼,但一旦完成,當然沒有其他線程會再這樣做。顯然,n次做同樣的事情是浪費時間。這裏的問題是,將使用鎖定保存你的任何時間或相反?只有性能測試才能給你答案。除非有其他原因不使用鎖。