Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
En muchos casos, PLINQ puede proporcionar mejoras de rendimiento significativas en las consultas LINQ to Objects secuenciales. Sin embargo, el trabajo de paralelizar la ejecución de la consulta presenta complejidad que puede provocar problemas que, en código secuencial, no son tan comunes o no se encuentran en absoluto. En este tema se enumeran algunas prácticas que se deben evitar al escribir consultas PLINQ.
No supongamos que el paralelo siempre es más rápido
La paralelización a veces hace que una consulta PLINQ se ejecute más lentamente que su equivalente de LINQ to Objects. La regla general es que las consultas que tienen pocos elementos de origen y delegados de usuario rápidos no suelen aumentar mucho la velocidad. Sin embargo, dado que muchos factores intervienen en el rendimiento, se recomienda medir los resultados reales antes de decidir si usar PLINQ. Para obtener más información, consulte Comprender la aceleración en PLINQ.
Evitar la escritura en ubicaciones de memoria compartida
En código secuencial, no es raro leer o escribir en variables estáticas o campos de clase. Sin embargo, cada vez que varios subprocesos tienen acceso simultáneamente a estas variables, hay grandes posibilidades de que se produzcan condiciones de carrera. Aunque puede usar bloqueos para sincronizar el acceso a la variable, el costo de sincronización puede afectar al rendimiento. Por lo tanto, se recomienda evitar, o al menos limitar, el acceso al estado compartido en una consulta PLINQ tanto como sea posible.
Ejemplo: Condición de carrera con memoria compartida
En el ejemplo siguiente se muestra una condición de carrera que se produce cuando varios subprocesos escriben en una variable compartida. Se accede a la variable y se modifica simultáneamente mediante varios subprocesos sin sincronización, lo que conduce a resultados impredecibles:
static void DemonstrateRaceCondition()
{
int total = 0;
var numbers = Enumerable.Range(0, 10000);
// UNSAFE: Multiple threads writing to shared variable
numbers.AsParallel().ForAll(n => total += n);
Console.WriteLine($"Total (with race condition): {total}");
// Expected: 49,995,000 but result is unpredictable due to race condition
}
Shared Sub DemonstrateRaceCondition()
Dim total As Integer = 0
Dim numbers = Enumerable.Range(0, 10000)
' UNSAFE: Multiple threads writing to shared variable
numbers.AsParallel().ForAll(Sub(n) total += n)
Console.WriteLine($"Total (with race condition): {total}")
' Expected: 49,995,000 but result is unpredictable due to race condition
End Sub
En este código, la operación no es atómica. Implica leer el valor actual de , agregar y escribir el resultado en . Cuando varios subprocesos ejecutan esta operación simultáneamente, pueden leer el mismo valor, agregarlo en subprocesos diferentes y escribir los resultados que se sobrescriben entre sí. Esto hace que se pierdan algunas adiciones, lo que produce un resultado final incorrecto.
El enfoque correcto consiste en usar operaciones seguras para subprocesos que no requieren un estado mutable compartido:
static void DemonstrateCorrectApproach()
{
var numbers = Enumerable.Range(0, 10000);
// SAFE: Use thread-safe aggregate operation
int total = numbers.AsParallel().Sum();
Console.WriteLine($"Total (correct): {total}");
// Result is always 49,995,000
}
Shared Sub DemonstrateCorrectApproach()
Dim numbers = Enumerable.Range(0, 10000)
' SAFE: Use thread-safe aggregate operation
Dim total As Integer = numbers.AsParallel().Sum()
Console.WriteLine($"Total (correct): {total}")
' Result is always 49,995,000
End Sub
El método gestiona la paralelización internamente de manera segura para los hilos, asegurando resultados correctos sin necesidad de sincronización manual. Otros enfoques seguros incluyen el uso de agregaciones personalizadas o la recopilación de resultados en colecciones seguras para subprocesos, como .
Evitar la sobrelelización
Si usa el método , incurrirá en costos de sobrecarga al crear particiones de la colección de origen y sincronizar los subprocesos de trabajo. Las ventajas de la paralelización están limitadas aún más por el número de procesadores del equipo. Si se ejecutan varios subprocesos enlazados a cálculos en un único procesador, no se gana en velocidad. Por tanto, debe tener cuidado para no paralelizar en exceso una consulta.
El escenario más común en el que se puede producir la sobreparalelización es en consultas anidadas, como se muestra en el siguiente fragmento de código.
var q = from cust in customers.AsParallel()
from order in cust.Orders.AsParallel()
where order.OrderDate > date
select new { cust, order };
Dim q = From cust In customers.AsParallel()
From order In cust.Orders.AsParallel()
Where order.OrderDate > aDate
Select New With {cust, order}
En este caso, es mejor paralelizar solo el origen de datos externo (clientes) a menos que se apliquen una o varias de las condiciones siguientes:
La fuente de datos interna (cust.Orders) es conocida por ser muy larga.
Estás realizando un cálculo costoso en cada pedido. (la operación que se muestra en el ejemplo no es costosa).
Se sabe que el sistema de destino tiene suficientes procesadores para controlar el número de subprocesos que se producirán al paralelizar la consulta en .
En todos los casos, la mejor manera de determinar la forma de consulta óptima es probar y medir. Para obtener más información, vea Cómo: Medir el rendimiento de las consultas PLINQ.
Evitar llamadas a métodos que no son seguros para subprocesos
La escritura en métodos de instancia que no son seguros para subprocesos de una consulta PLINQ puede producir daños en los datos, que pueden pasar o no inadvertidos para el programa. También puede dar lugar a excepciones. En el siguiente ejemplo, varios subprocesos estarían intentando llamar simultáneamente al método , lo que no se admite en la clase.
Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));
Limitar las llamadas a métodos seguros para subprocesos
La mayoría de los métodos estáticos de .NET son seguros para subprocesos y se pueden llamar desde varios subprocesos simultáneamente. Sin embargo, incluso en estos casos, la sincronización que esto supone puede conducir a una ralentización importante en la consulta.
Nota:
Puede probar esto tú mismo insertando algunas llamadas a en tus consultas. Aunque este método se usa en los ejemplos de documentación con fines de demostración, no lo use en las consultas PLINQ.
Evitar operaciones de ordenación innecesarias
Cuando PLINQ ejecuta una consulta en paralelo, divide la secuencia de origen en particiones que se pueden operar simultáneamente en varios subprocesos. De forma predeterminada, el orden en el que se procesan las particiones y los resultados se entregan no son predecibles (excepto para operadores como ). Puede indicar a PLINQ que conserve la ordenación de cualquier secuencia de origen, pero esto tiene un impacto negativo en el rendimiento. El procedimiento recomendado, siempre que sea posible, consiste en estructurar las consultas para que no se basen en la conservación del orden. Para obtener más información, vea Conservación de pedidos en PLINQ.
Preferir ForAll a ForEach cuando sea posible
Aunque PLINQ ejecuta una consulta en varios subprocesos, si consume los resultados en un bucle foreach (For Each en Visual Basic), los resultados de la consulta deben combinarse de nuevo en un solo subproceso para que el enumerador acceda a ellos en serie. En algunos casos, esto es inevitable; sin embargo, siempre que sea posible, use el método para habilitar cada subproceso para generar sus propios resultados, por ejemplo, escribiendo en una colección segura para subprocesos como .
El mismo problema se aplica a . En otras palabras, debe preferirse encarecidamente a .
Tenga en cuenta los posibles problemas de afinidad de hilos
Algunas tecnologías, por ejemplo, la interoperabilidad COM para componentes de Single-Threaded Apartment (STA), Windows Forms y Windows Presentation Foundation (WPF), imponen restricciones de afinidad de subprocesos que requieren que el código se ejecute en un subproceso específico. Por ejemplo, tanto en Windows Forms como en WPF, solo se puede tener acceso a un control en el subproceso en el que se creó. Si intenta acceder al estado compartido de un control de Windows Forms en una consulta PLINQ, se produce una excepción si se ejecuta en el depurador. (Esta configuración se puede desactivar). Sin embargo, si la consulta se consume en el subproceso de la interfaz de usuario, puede acceder al control desde el bucle que enumera los resultados de la consulta porque ese código se ejecuta en un solo subproceso.
No suponga que las iteraciones de ForEach, For y ForAll siempre se ejecutan en paralelo
Es importante tener en cuenta que las iteraciones individuales de un bucle , o pueden pero no tener que ejecutarse en paralelo. Por consiguiente, se debe evitar escribir código cuya exactitud dependa de la ejecución en paralelo de las iteraciones o de la ejecución de las iteraciones en algún orden concreto.
Por ejemplo, es probable que este código lleve a un interbloqueo:
Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll(Sub(j)
If j = Environment.ProcessorCount Then
Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
mre.Set()
Else
Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
mre.Wait()
End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll((j) =>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
mre.Set();
}
else
{
Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
mre.Wait();
}
}); //deadlocks
En este ejemplo, una iteración establece un evento y el resto de las iteraciones esperan el evento. Ninguna de las iteraciones que esperan puede completarse hasta que se haya completado la iteración que establece el evento. Sin embargo, es posible que las iteraciones que esperan bloqueen todos los subprocesos que se utilizan para ejecutar el bucle paralelo antes de que la iteración de configuración del evento haya tenido la oportunidad de ejecutarse. Esto produce un interbloqueo: la iteración de la configuración del evento nunca se ejecutará y las iteraciones que esperan nunca se despertarán.
En concreto, una iteración de un bucle paralelo no debe esperar nunca otra iteración del bucle para progresar. Si el bucle paralelo decide programar las iteraciones secuencialmente pero en el orden contrario, se producirá un interbloqueo.
Consulte también
- LINQ paralelo (PLINQ)