2010-07-29 28 views
17

Tengo un loop foreach que estoy paralelizando y noté algo extraño. El código es el siguienteDiferentes resultados de suma con Parallel.ForEach

double sum = 0.0; 

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

// Use sum variable below 

Cuando uso una foreach bucle regulares consigo resultados diferentes. Puede haber algo más profundo en el interior del ComplicatedFunction, pero es posible que la variable sum no se vea afectada por la paralelización?

+1

Ver [ incremento de un valor de conteo fuera de alcance parallel.foreach ] (http://stackoverflow.com/questions/2394447/increment-a-count-value-outside-parallel-foreach-scope). Básicamente, puede usar [Interlocked] (http://msdn.microsoft.com/en-us/library/55dzx06b.aspx) si lo necesita, pero es mejor evitar los efectos secundarios si es posible. –

Respuesta

28

¿Es posible que la variable de suma no se vea afectada por la paralelización?

Sí.
El acceso a un double no es atómico y la operación sum += ... nunca es segura para subprocesos, ni siquiera para los tipos que son atómicos. Entonces tienes múltiples condiciones de carrera y el resultado es impredecible.

Se podría utilizar algo como:

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

o, en una notación más corta

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

Esto funcionó. Buena implementación limpia usando LINQ. –

+0

Según la publicación original, ¿es necesario que 'myCollection' sea seguro para subprocesos? –

+0

@Kevin - No, cualquier 'IEnumerable ' funcionará. –

4

Si se piensa que sum += ComplicatedFunction que se compone realmente de un grupo de operaciones, dice:

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

Así que ahora intercala aleatoriamente dos (o más) instancias paralelas de esto. Un hilo puede contener un "valor antiguo" añejo de la suma que utiliza para realizar su cálculo, cuyo resultado se escribe sobre la parte superior de alguna versión modificada de la suma. Es una condición de carrera clásica, porque algunos resultados se pierden de una manera no determinista en función de cómo se realiza el entrelazado.

+5

Tiene razón, pero la situación es mucho peor de lo que indica. No es solo que las operaciones de carga, cálculo y almacenamiento no son atómicas. ¡El acceso a los * bits * en un doble ni siquiera se garantiza que sea atómico! La especificación C# solo garantiza que el acceso a tipos numéricos y referencias de 32 bits (y menores) son atómicos. Los dobles son de 64 bits y, por lo tanto, no se garantiza que sean atómicos. Este programa podría realizarse como: r1 <- cargar los 32 bits superiores de la suma, r1 <- cargar los 32 bits inferiores de la suma ... lo que significa que las operaciones podrían intercalarse mientras se ha copiado * la mitad * del doble. –

+0

bien dicho. Pensé que por simplicidad en el ejemplo, simplemente asumiría la atomicidad de las operaciones básicas, pero obviamente, como usted señala, el peor de los casos es aún más horrible. – Gian

11

Al igual que las otras respuestas mencionadas, la actualización de la variable sum de varios subprocesos (que es lo que hace Parallel.ForEach) no es una operación segura para subprocesos. La solución trivial de adquirir un bloqueo antes de realizar la actualización solucionará ese problema.

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

Sin embargo, que introduce otro problema. Dado que el bloqueo se adquiere en cada iteración, eso significa que la ejecución de cada iteración se serializará efectivamente. En otras palabras, hubiera sido mejor usar un simple loop viejo foreach.

Ahora, el truco para hacer esto bien es dividir el problema en mandriles separados e independientes. Afortunadamente, es muy fácil hacerlo cuando todo lo que desea hacer es sumar el resultado de las iteraciones porque la operación suma es conmutativa y asociativa, y porque los resultados intermedios de las iteraciones son independientes.

Así que aquí está cómo lo haces.

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

¿Por qué estás fomentando la reinvención de una rueda muy estándar? – Novelocrat

+0

@Novelocrat: Lo siento, no tengo claro lo que estás preguntando. Además, debido al tiempo, ¿puedo suponer que votaste negativamente esta respuesta? Si es así, ¿qué parte de la respuesta es incorrecta? Comprobé dos veces la sintaxis del código y la estrategia de partición es una práctica bastante establecida para hacer operaciones 'Parallel.For', pero tal vez me perdí algo que llamó tu atención. –

+0

Lo que la implementación describe es exactamente la función de biblioteca que describe la respuesta de Henk. Además, sospecho fuertemente que la reducción del subtotal de cada hilo ('Acumulador') en la implementación de la biblioteca es mucho más eficiente que su enfoque basado en el bloqueo. – Novelocrat

Cuestiones relacionadas