2010-07-29 101 views
17

我有一個foreach循環,我正在進行並行處理,並且發現了一些奇怪的東西。代碼看起來像Parallel.ForEach的不同求和結果

double sum = 0.0; 

Parallel.ForEach(myCollection, arg => 
{ 
    sum += ComplicatedFunction(arg); 
}); 

// Use sum variable below 

當我使用一個普通foreach環路我得到不同的結果。 ComplicatedFunction內部可能存在更深層次的內容,但sum變量可能會受到並行化的意外影響嗎?

+1

見[ 增量外parallel.foreach範圍 的計數值(http://stackoverflow.com/questions/2394447/increment-a-count-value-outside-parallel-foreach-scope)。基本上,如果需要,您可以使用[Interlocked](http://msdn.microsoft.com/en-us/library/55dzx06b.aspx),但如果可能的話,最好避免使用副作用。 – 2010-07-29 22:03:33

回答

28

總和變量可能受到平行化意外影響?

是的。
double的訪問不是原子操作,sum += ...操作永遠不會線程安全,即使對於原子類型也是如此。所以你有多種競爭條件,結果是不可預測的。

你可以使用這樣的:如果你想想sum += ComplicatedFunction爲被實際上是由一堆操作的

double sum = myCollection.AsParallel().Sum(arg => ComplicatedFunction(arg)); 

,或者在更短的符號

double sum = myCollection.AsParallel().Sum(ComplicatedFunction); 
+0

這工作。使用LINQ很好的乾淨的實現。 – 2010-07-30 14:07:18

+0

對於原始帖子,「myCollection」是否必須是線程安全的? – 2013-08-06 14:30:01

+0

@Kevin - 不,任何'IEnumerable '都行。 – 2013-08-06 14:34:44

4

,說:

r1 <- Load current value of sum 
r2 <- ComplicatedFunction(...) 
r1 <- r1 + r2 

所以現在我們隨機交錯這兩個(或更多)並行實例。一個線程可能持有一個陳舊的「舊值」,它用於執行其計算,其結果會寫回總和的某個修改版本的頂部。這是一個典型的競爭條件,因爲根據交錯如何完成,一些結果以非確定性的方式迷失。

+5

你說得對,但事實上情況實際上比你說的要糟糕得多。這不僅僅是加載,計算和存儲操作不是原子的。在double中訪問*位*甚至不能保證是原子的! C#規範只保證訪問32位(或更小)的數字類型和引用是原子的。加倍是64位,因此不保證是原子的。這個程序可以被實現爲:r1 < - 加載總和的前32位,r1 < - 加載總和的下32位...意味着操作可以被交錯,而加倍的*半*被複制。 – 2010-07-29 23:47:22

+0

說得好。在這個例子中,我簡單地假設了基本操作的原子性,但是正如你指出的那樣,最壞的情況更加可怕。 – Gian 2010-07-30 00:13:22

11

像其他人提到的答案一樣,從多個線程更新sum變量(這是Parallel.ForEach的作用)不是線程安全操作。在進行更新之前獲取鎖的微小修復將修復問題。

double sum = 0.0; 
Parallel.ForEach(myCollection, arg => 
{ 
    lock (myCollection) 
    { 
    sum += ComplicatedFunction(arg); 
    } 
}); 

然而,這引入了另一個問題。由於鎖是在每次迭代中獲取的,那麼這意味着每次迭代的執行將被有效地序列化。換句話說,使用簡單的舊的foreach循環會更好。

現在,獲得這一權利的訣竅是將問題分割成獨立的卡盤。幸運的是,當所有你想做的事情總結迭代的結果時,這是非常容易的,因爲和運算是交換和關聯的,並且迭代的中間結果是獨立的。

所以這裏是你如何做到這一點。

double sum = 0.0; 
Parallel.ForEach(myCollection, 
    () => // Initializer 
    { 
     return 0D; 
    }, 
    (item, state, subtotal) => // Loop body 
    { 
     return subtotal += ComplicatedFunction(item); 
    }, 
    (subtotal) => // Accumulator 
    { 
     lock (myCollection) 
     { 
      sum += subtotal; 
     } 
    }); 
+1

你爲什麼鼓勵重塑一個非常標準的車輪? – Novelocrat 2010-08-02 13:44:56

+0

@Novelocrat:對不起,我不清楚你在問什麼。另外,由於時間的關係,我可以假設你低估了這個答案?如果是的話答案的哪一部分是錯誤的?我仔細檢查了代碼語法,並且分區策略對於執行「Parallel.For」操作是相當成熟的做法,但也許我錯過了引起您注意的事情。 – 2010-08-02 17:49:47

+0

你描述的實現的東西正是Henk的答案所描述的庫函數。此外,我強烈懷疑庫實現中每個線程小計('累加器')的減少比基於鎖定的方法效率更高。 – Novelocrat 2010-08-02 17:55:54