2010-07-29 11 views
17

Ich habe eine foreach Schleife, die ich parallelisiere und mir ist etwas merkwürdig aufgefallen. Der Code sieht aus wieUnterschiedliche Summierungsergebnisse mit Parallel.ForEach

double sum = 0.0; 

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

// Use sum variable below 

Wenn ich eine regelmäßige foreach Schleife verwende ich unterschiedliche Ergebnisse erhalten. Es kann etwas tiefer in der ComplicatedFunction sein, aber es ist möglich, dass die sum Variable unerwartet von der Parallelisierung betroffen ist?

+1

See [ Schritt ein Zählwert außerhalb Parallel.ForEach Umfang ] (http://stackoverflow.com/questions/2394447/increment-a-count-value-outside-parallel-foreach-scope). Grundsätzlich können Sie [Interlocked] (http://msdn.microsoft.com/en-us/library/55dzx06b.aspx) verwenden, wenn es nötig ist, aber es ist besser, wenn möglich Nebenwirkungen zu vermeiden. –

Antwort

28

ist es möglich, dass die Summenvariable unerwartet von der Parallelisierung betroffen ist?

Ja.
Der Zugriff auf eine double ist nicht atomar und die sum += ... Operation ist nie threadsicher, nicht einmal für atomare Typen. Sie haben also mehrere Rennbedingungen und das Ergebnis ist unvorhersehbar.

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

oder

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

Das hat funktioniert. Schöne, saubere Implementierung mit LINQ. –

+0

Ist es für den ursprünglichen Post erforderlich, dass "meineCollection" Thread-sicher ist? –

+0

@Kevin - Nein, keine 'IEnumerable ' wird tun. –

4

Wenn Sie darüber nachdenken, dass sum += ComplicatedFunction als eigentlich aus einer Reihe von Operationen, etwa in einer kürzeren Schreibweise:

Sie so etwas wie verwenden könnte

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

Also verschachteln wir zufällig zwei (oder mehr) parallele Instanzen davon. Ein Thread kann einen veralteten "alten Wert" der Summe halten, den er benutzt, um seine Berechnung durchzuführen, dessen Ergebnis er über eine modifizierte Version der Summe zurückschreibt. Es ist eine klassische Race-Bedingung, weil einige Ergebnisse nichtdeterministisch verloren gehen, je nachdem, wie das Interleaving durchgeführt wird.

+5

Sie haben Recht, aber die Situation ist in der Tat viel schlimmer als Sie sagen. Es ist nicht nur so, dass die Lade-, Berechnungs- und Speicheroperationen nicht atomar sind. Der Zugriff auf die * Bits * in einem Double ist nicht einmal garantiert atomar! Die C# -Spezifikation garantiert nur, dass der Zugriff auf 32-Bit- (und kleinere) numerische Typen und Referenzen atomar ist. Doppel sind 64 Bit und daher nicht garantiert atomar. Dieses Programm könnte wie folgt realisiert werden: r1 <- lade die obersten 32 Bits der Summe, r1 <- lade die unteren 32 Bits der Summe ... was bedeutet, dass die Operationen verschachtelt werden können, während * die Hälfte * des Doppelwerts kopiert wurde. –

+0

Gut gesagt. Ich dachte mir einfach, in dem Beispiel würde ich einfach Atomizität der Grundoperationen annehmen, aber offensichtlich, wie Sie darauf hinweisen, ist der schlimmste Fall sogar noch schlimmer. – Gian

11

Wie die anderen Antworten erwähnt, ist das Aktualisieren der sum Variable aus mehreren Threads (was Parallel.ForEach tut) keine Thread-sichere Operation. Die triviale Behebung des Erlangens einer Sperre vor der Aktualisierung behebt das Problem.

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

Allerdings führt das ein weiteres Problem ein. Da die Sperre bei jeder Iteration akquiriert wird, bedeutet dies, dass die Ausführung jeder Iteration effektiv serialisiert wird. Mit anderen Worten, es wäre besser gewesen, einfach einen einfachen alten foreach Loop zu verwenden.

Jetzt ist der Trick, dies richtig zu machen, das Problem in separate und unabhängige Chucks zu teilen. Glücklicherweise ist das sehr einfach zu tun, wenn Sie nur das Ergebnis der Iterationen summieren wollen, weil die Summenoperation kommutativ und assoziativ ist und weil die Zwischenergebnisse der Iterationen unabhängig sind.

Also hier ist, wie Sie es tun.

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

Warum ermutigen Sie die Neuerfindung eines sehr standardmäßigen Rades? – Novelocrat

+0

@Novelocrat: Entschuldigung, ich bin nicht klar, was Sie fragen. Kann ich aufgrund des Timings davon ausgehen, dass Sie diese Antwort abgelehnt haben? Wenn ja, welcher Teil der Antwort ist falsch? Ich überprüfte die Codesyntax zweimal und die Partitionierungsstrategie ist eine ziemlich gut etablierte Praxis für 'Parallel.For'-Operationen, aber vielleicht habe ich etwas übersehen, was dir aufgefallen ist. –

+0

Das Ding, dessen Implementierung Sie beschreiben, ist genau die Bibliotheksfunktion, die Henks Antwort beschreibt. Darüber hinaus vermute ich stark, dass die Reduzierung der Zwischensumme jedes Threads ("Accumulator") in der Bibliotheksimplementierung viel effizienter ist als Ihr Lock-basierter Ansatz. – Novelocrat

Verwandte Themen