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.
Evitar la sobrelelización
Si usa el método AsParallel
, 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.
Se realiza 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
cust.Orders
.
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 FileStream.Write
, 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 les puede llamar desde varios 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 comprobarlo si inserta algunas llamadas a WriteLine en las 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 OrderBy
). 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 foreach
bucle (For Each
en Visual Basic), los resultados de la consulta se deben combinar de nuevo en un subproceso y el enumerador tiene acceso a ellos en serie. En algunos casos, esto es inevitable; sin embargo, siempre que sea posible, use el ForAll
método para habilitar cada subproceso para generar sus propios resultados, por ejemplo, escribiendo en una colección segura para subprocesos como System.Collections.Concurrent.ConcurrentBag<T>.
El mismo problema se aplica a Parallel.ForEach. En otras palabras, source.AsParallel().Where().ForAll(...)
debe preferirse encarecidamente a Parallel.ForEach(source.AsParallel().Where(), ...)
.
Tenga en cuenta los posibles problemas de afinidad de hilos
Algunas tecnologías, como la interoperabilidad COM para componentes de contenedor uniproceso (STA), Windows Forms y Windows Presentation Foundation (WPF), imponen restricciones de afinidad de subprocesos que exigen que el código se ejecute en un subproceso determinado. Por ejemplo, tanto en Windows Forms como en WPF, solo se puede tener acceso a un control en el subproceso donde se creó. Si intenta tener acceso al estado compartido de un control de formularios 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 foreach
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 Parallel.Forbucle , Parallel.ForEacho ForAll 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 del valor de 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 del valor de evento haya tenido oportunidad de ejecutarse. Esto produce un interbloqueo: la iteración del valor de evento nunca se ejecutará y las iteraciones que esperan nunca se activará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.