Introducción a PLINQ

Parallel LINQ (PLINQ) es una implementación en paralelo del patrón Language-Integrated Query (LINQ). PLINQ implementa el conjunto completo de operadores de consulta estándar de LINQ como métodos de extensión para el espacio de nombres System.Linq y tiene operadores adicionales para las operaciones en paralelo. PLINQ combina la simplicidad y legibilidad de la sintaxis de LINQ con la eficacia de la programación en paralelo.

Sugerencia

Si no está familiarizado con LINQ, ofrece un modelo unificado para consultar cualquier origen de datos enumerable en un modo con seguridad de tipos. LINQ to Objects es el nombre de las consultas LINQ que se ejecutan en colecciones en memoria, como List<T> y matrices. En este artículo, se da por echo que posee un conocimiento básico de LINQ. Para obtener más información, vea Language Integrated Query (LINQ).

¿Qué es una consulta en paralelo?

En muchos sentidos, una consulta PLINQ se parece a una consulta LINQ to Objects no en paralelo. Las consultas PLINQ, al igual que las consultas LINQ secuenciales, funcionan en cualquier origen de datos IEnumerable o IEnumerable<T> en memoria y tienen ejecución aplazada, lo que significa que no se empiezan a ejecutar hasta que se enumera la consulta. La principal diferencia es que PLINQ intenta usar completamente todos los procesadores del sistema. Para ello, crea particiones del origen de datos en segmentos y, luego, ejecuta la consulta en cada segmento en subprocesos de trabajo independientes en paralelo en varios procesadores. En muchos casos, la ejecución en paralelo significa que la consulta se ejecuta considerablemente más rápido.

A través de la ejecución en paralelo, PLINQ puede alcanzar importantes mejoras de rendimiento con respecto al código heredado en ciertos tipos de consultas, a menudo solo con agregar la operación de consulta AsParallel al origen de datos. Sin embargo, el paralelismo puede presentar sus propias complejidades y no todas las operaciones de consulta se ejecutan más rápido en PLINQ. De hecho, la paralelización en realidad ralentiza ciertas consultas. Por lo tanto, debe entender cómo ciertos problemas, como la ordenación, afectan a las consultas en paralelo. Para más información, consulte Introducción a la velocidad en PLINQ.

Nota

En esta documentación, se utilizan expresiones lambda para definir delegados en PLINQ. Si no está familiarizado con las expresiones lambda de C# o Visual Basic, consulte Expresiones lambda en PLINQ y TPL.

En el resto del artículo se ofrece información general de las principales clases PLINQ y se analiza cómo crear consultas PLINQ. Cada sección incluye vínculos a información más detallada y ejemplos de código.

La clase ParallelEnumerable

La clase System.Linq.ParallelEnumerable expone casi toda la funcionalidad de PLINQ. Junto con el resto de los tipos de espacios de nombres System.Linq, se compilan en el ensamblado System.Core.dll. Los proyectos C# y Visual Basic predeterminados en Visual Studio hacen referencia al ensamblado e importan el espacio de nombres.

ParallelEnumerable incluye implementaciones de todos los operadores de consulta estándar que LINQ to Objects admite, a pesar de que no intenta ejecutar en paralelo cada uno de ellos. Si no está familiarizado con LINK, consulte Introducción a LINQ (C#) e Introducción a LINQ (Visual Basic).

Además de los operadores de consulta estándar, la clase ParallelEnumerable contiene un conjunto de métodos que permiten comportamientos específicos de la ejecución en paralelo. Estos métodos específicos de PLINQ se muestran en la tabla siguiente.

Operador ParallelEnumerable Descripción
AsParallel Punto de entrada de PLINQ. Especifica que el resto de la consulta se debe ejecutar en paralelo, si es posible.
AsSequential Especifica que el resto de la consulta se debe ejecutar en secuencia, como una consulta LINQ no en paralelo.
AsOrdered Especifica que PLINQ debe conservar la ordenación de la secuencia de origen para el resto de la consulta o hasta que cambie la ordenación, por ejemplo, mediante el uso de una cláusula orderby (Order By en Visual Basic).
AsUnordered Especifica que PLINQ no necesita conservar la ordenación de la secuencia de origen para el resto de la consulta.
WithCancellation Especifica que PLINQ debe supervisar periódicamente el estado del token de cancelación que se proporciona y cancelar la ejecución si se solicita.
WithDegreeOfParallelism Especifica la cantidad máxima de procesadores que PLINQ debe usar para ejecutar la consulta en paralelo.
WithMergeOptions Proporciona una sugerencia sobre cómo PLINQ debería, si es posible, combinar los resultados en paralelo en una sola secuencia en el subproceso utilizado.
WithExecutionMode Especifica si PLINQ debe ejecutar la consulta en paralelo incluso si el comportamiento predeterminado indica que se debería ejecutar en secuencia.
ForAll Método de enumeración multiproceso que, a diferencia de la iteración sobre los resultados de la consulta, permite procesar los resultados en paralelo sin tener que combinarlos primero en el subproceso del consumidor.
Aggregate overload Sobrecarga única para PLINQ que permite la agregación inmediata sobre particiones locales de subprocesos, además de una función de agregación local para combinar los resultados de todas las particiones.

El modelo de participación

Cuando escribe una consulta, participa en PLINQ al invocar el método de extensión ParallelEnumerable.AsParallel en el origen de datos, tal como se muestra en el ejemplo siguiente.

var source = Enumerable.Range(1, 10000);

// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
               where num % 2 == 0
               select num;
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count());
// The example displays the following output:
//       5000 even numbers out of 10000 total
Dim source = Enumerable.Range(1, 10000)

' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
               Where num Mod 2 = 0
               Select num
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count())
' The example displays the following output:
'       5000 even numbers out of 10000 total

El método de extensión AsParallel enlaza los operadores de consulta subsiguientes, en este caso, where y select, a las implementaciones System.Linq.ParallelEnumerable.

Modos de ejecución

De manera predeterminada, PLINQ es conservador. En tiempo de ejecución, la infraestructura de PLINQ analiza la estructura general de la consulta. Si existe la probabilidad de que la consulta genere aumentos de velocidad a través de la paralelización, PLINQ particiona la secuencia de origen en tareas que se pueden ejecutar en simultáneo. Si ejecutar una consulta en paralelo no es seguro, PLINQ simplemente la ejecuta en secuencia. Si PLINQ puede elegir entre un algoritmo en paralelo posiblemente costoso y un algoritmo secuencial económico, de manera predeterminada elegirá el algoritmo secuencial. Puede usar el método WithExecutionMode y la enumeración System.Linq.ParallelExecutionMode para indicar a PLINQ que seleccione el algoritmo paralelo. Esto resulta útil cuando, a través de pruebas y mediciones, sabe que una consulta determinada se ejecuta más rápido en paralelo. Para obtener más información, vea Cómo: Especificar el modo de ejecución en PLINQ.

Grado de paralelismo

De manera predeterminada, PLINQ usa todos los procesadores del equipo host. Puede indicar a PLINQ que use más de un número especificado de procesadores mediante el método WithDegreeOfParallelism. Esto resulta útil cuando desea asegurarse de que otros procesos que estén en ejecución en el equipo reciban cierta cantidad de tiempo de CPU. El siguiente fragmento de código limita la consulta para que solo use un máximo de dos procesadores.

var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
            where Compute(item) > 42
            select item;
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
            Where Compute(item) > 42
            Select item

En casos en los que una consulta realiza una cantidad considerable de trabajo no enlazado a proceso, como E/S de archivo, puede se recomendable especificar un grado de paralelismo mayor que el número de núcleos de la máquina.

Comparación entre consultas en paralelo ordenadas y no ordenadas

En algunas consultas, un operador de consulta debe generar resultados que conserven la ordenación de la secuencia de origen. PLINQ proporciona el operador AsOrdered para este propósito. AsOrdered es distinto de AsSequential. Una secuencia AsOrdered de todos modos se procesa en paralelo, pero sus resultados se almacenan en búfer y se ordenan. Dado que conservar el orden implica normalmente trabajo adicional, una secuencia AsOrdered se podría procesar más despacio que la secuencia AsUnordered predeterminada. El hecho que una operación en paralelo ordenada determinada sea más rápida que una versión secuencial de la operación depende de muchos factores.

En el ejemplo de código siguiente se muestra cómo participar en conservar el orden.

var evenNums =
    from num in numbers.AsParallel().AsOrdered()
    where num % 2 == 0
    select num;
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
               Where num Mod 2 = 0
               Select num


Para más información, consulte cómo conservar el orden en PLINQ.

Comparación entre consultas en paralelo y consultas secuenciales

Algunas operaciones requieren que los datos de origen se entreguen en secuencia. Los operadores de consulta ParallelEnumerable vuelven automáticamente al modo secuencial cuando es necesario. Para los operadores de consulta definidos por el usuario y los delegados de usuario que requieren la ejecución secuencial, PLINQ proporciona el método AsSequential. Cuando se usa AsSequential, todos los operadores subsiguientes de la consulta se ejecutan secuencialmente hasta que se vuelve a llamar a AsParallel. Para obtener más información, vea Cómo: Combinar consultas LINQ paralelas y secuenciales.

Opciones para combinar resultados de consulta

Cuando una consulta PLINQ se ejecuta en paralelo, sus resultados a partir de cada subproceso de trabajo se deben volver a combinar en el subproceso principal para que los use un bucle foreach (For Each en Visual Basic) o para su inserción en una lista o matriz. En algunos casos, podría ser recomendable especificar un tipo determinado de operación Merge, por ejemplo, para comenzar a generar resultados más rápidamente. Para este propósito, PLINQ admite el método WithMergeOptions y la enumeración ParallelMergeOptions. Para más información, consulte las opciones de combinación en PLINQ.

El operador ForAll

En las consultas LINK secuenciales, la ejecución se aplaza hasta que la consulta se enumera en un bucle foreach (For Each en Visual Basic) o mediante la invocación de un método como ToList, ToArray o ToDictionary. En PLINQ, también puede usar foreach para ejecutar la consulta e iterar a través de los resultados. Sin embargo, foreach no se ejecuta en paralelo y, por lo tanto, requiere que el resultado de todas las tareas en paralelo se combinen nuevamente en el subproceso en el que se ejecuta el bucle. En PLINQ, puede usar foreach cuando deba conservar la ordenación final de los resultados de la consulta y también cada vez que procese los resultados de forma serial; por ejemplo, cuando llama a Console.WriteLine para cada elemento. Para lograr un ejecución más rápida de las consultas cuando no es necesario conservar el orden y cuando el procesamiento mismo de los resultados se puede ejecutar en paralelo, use el método ForAll para ejecutar una consulta PLINQ. ForAll no lleva a cabo este paso de combinación final. En el ejemplo de código siguiente, se muestra cómo se utiliza el método ForAll. Aquí se usa System.Collections.Concurrent.ConcurrentBag<T> porque está optimizado para varios subprocesos, lo que agrega simultaneidad sin intentar quitar ningún elemento.

var nums = Enumerable.Range(10, 10000);
var query =
    from num in nums.AsParallel()
    where num % 10 == 0
    select num;

// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll(e => concurrentBag.Add(Compute(e)));
Dim nums = Enumerable.Range(10, 10000)
Dim query = From num In nums.AsParallel()
            Where num Mod 10 = 0
            Select num

' Process the results as each thread completes
' and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
' which can safely accept concurrent add operations
query.ForAll(Sub(e) concurrentBag.Add(Compute(e)))

En la ilustración siguiente se muestra la diferencia entre foreach y ForAll con respecto a la ejecución de la consulta.

ForAll en comparación con ForEach

Cancelación

PLINQ se integra con los tipos de cancelación en .NET. (Para más información, consulte el tema sobre la cancelación en subprocesos administrados). Por lo tanto, a diferencia de las consultas LINQ to Objects secuenciales, las consultas PLINQ se pueden cancelar. Para crear una consulta PLINQ cancelable, use el operador WithCancellation en la consulta y proporcione una instancia CancellationToken como argumento. Cuando la propiedad IsCancellationRequested del token se establece en true, PLINQ lo tendrá en cuenta, detendrá el procesamiento de todos los subprocesos e iniciará OperationCanceledException.

Es posible que una consulta PLINQ siga procesando algunos elementos una vez establecido el token de cancelación.

Para una mayor capacidad de respuesta, también puede responder a las solicitudes de cancelación en delegados de usuario de ejecución prolongada. Para obtener más información, vea Cómo: Cancelar una consulta PLINQ.

Excepciones

Cuando se ejecuta una consulta PLINQ, distintos subprocesos pueden generar varias excepciones de forma simultánea. Además, el código para controlar la excepción puede estar en un subproceso distinto al del código que generó la excepción. PLINQ usa el tipo AggregateException para encapsular todas las excepciones que generó una consulta y calcular las referencias de esas excepciones nuevamente en el subproceso que realiza la llamada. En el subproceso que realiza la llamada, solo se requiere un bloque try-catch. Sin embargo, puede iterar a través de todas las excepciones que están encapsuladas en AggregateException y capturar cualquiera desde la que pueda realizar una recuperación de forma segura. En raras ocasiones, se pueden generar algunas excepciones que no se ajustan en AggregateException y ThreadAbortException tampoco se ajusta.

Cuando las excepciones pueden propagarse de nuevo al subproceso de unión, es posible que una consulta continúe procesando algunos elementos después de que se haya producido la excepción.

Para obtener más información, vea Cómo: Controlar excepciones en una consulta PLINQ.

Particionadores personalizados

En algunos casos, puede mejorar el rendimiento de las consultas si escribe un particionador personalizado que aproveche algunas características de los datos de origen. En la consulta, el mismo particionador personalizado es el objeto enumerable que se consulta.

int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
Dim arr(10000) As Integer
Dim partitioner As Partitioner(Of Integer) = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))

PLINQ admite una cantidad fija de particiones (a pesar de que los datos se pueden reasignar de forma dinámica a esas particiones durante el tiempo de ejecución para el equilibrio de carga). For y ForEach admiten solo la partición dinámica, lo que significa que el número de particiones cambia en tiempo de ejecución. Para más información, consulte Custom Partitioners for PLINQ and TPL (Particionadores personalizados para PLINQ y TPL).

Medición del rendimiento de PLINQ

En muchos casos, una consulta se puede ejecutar en paralelo, pero la sobrecarga que implica configurar la consulta en paralelo supera el beneficio obtenido en el rendimiento. Si una consulta no realiza mucho cálculo o si el origen de datos es pequeño, una consulta PLINQ podría ser más lenta que una consulta LINQ to Objects secuencial. Puede usar el analizador de rendimiento en paralelo en Visual Studio Team Server para comparar el rendimiento de diversas consultas, ubicar cuellos de botella en el procesamiento y determinar si la consulta se ejecuta en paralelo o secuencialmente. Para más información, vea Visualizador de simultaneidad y Cómo: Medir el rendimiento de consultas PLINQ.

Vea también