Compartir a través de


Introducción a PLINQ

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

Sugerencia

Si no está familiarizado con LINQ, incluye un modelo unificado para consultar cualquier origen de datos enumerable de forma segura. LINQ to Objects es el nombre de las consultas LINQ que se ejecutan en colecciones en memoria, como List<T> y arrays. En este artículo se da por supuesto que tiene un conocimiento básico de LINQ. Para obtener más información, consulte Language-Integrated Query (LINQ).

¿Qué es una consulta paralela?

Una consulta PLINQ de muchas maneras se parece a una consulta LINQ to Objects no paralela. Las consultas PLINQ, al igual que las consultas LINQ secuenciales, operan sobre cualquier origen de datos en memoria IEnumerable o IEnumerable<T> y tienen una ejecución diferida, lo que significa que no comienzan a ejecutarse hasta que se enumera la consulta. La principal diferencia es que PLINQ intenta hacer un uso completo de todos los procesadores del sistema. Para ello, particiona el origen de datos en segmentos y, a continuación, 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 significativamente más rápido.

A través de la ejecución en paralelo, PLINQ puede lograr mejoras de rendimiento significativas en el código heredado para determinados tipos de consultas, a menudo simplemente agregando la AsParallel operación de consulta al origen de datos. Sin embargo, el paralelismo puede introducir sus propias complejidades y no todas las operaciones de consulta se ejecutan más rápido en PLINQ. De hecho, la paralelización ralentiza realmente ciertas consultas. Por lo tanto, debe comprender cómo ciertos problemas, como la ordenación, afectan a las consultas paralelas. Para obtener más información, consulte Comprender la aceleración en PLINQ.

Nota:

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

En el resto de este artículo se proporciona información general sobre las clases PLINQ principales y se describe cómo crear consultas PLINQ. Cada sección contiene vínculos a información más detallada y ejemplos de código.

La clase ParallelEnumerable

La System.Linq.ParallelEnumerable clase 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 predeterminados de C# y Visual Basic 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 admite LINQ to Objects, aunque no intenta paralelizar cada uno. Si no está familiarizado con LINQ, consulte Introducción a LINQ (C#) e Introducción a LINQ (Visual Basic).

Además de los operadores de consulta estándar, la ParallelEnumerable clase 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 enumeran en la tabla siguiente.

Operador ParallelEnumerable Descripción
AsParallel Punto de entrada de PLINQ. Especifica que el resto de la consulta se debe paralelizar, si es posible.
AsSequential Especifica que el resto de la consulta se debe ejecutar secuencialmente, como una consulta LINQ no paralela.
AsOrdered Especifica que PLINQ debe conservar la ordenación de la secuencia de origen para el resto de la consulta, o hasta que se cambie el orden, por ejemplo mediante el uso de una cláusula orderby (Order By en Visual Basic).
AsUnordered Especifica que PLINQ para el resto de la consulta no es necesario para conservar la ordenación de la secuencia de origen.
WithCancellation Especifica que PLINQ debe supervisar periódicamente el estado del token de cancelación proporcionado y cancelar la ejecución si se solicita.
WithDegreeOfParallelism Especifica el número máximo de procesadores que PLINQ debe usar para paralelizar la consulta.
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 paralelizar la consulta incluso cuando el comportamiento predeterminado sería ejecutarla secuencialmente.
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 sobrecarga 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.

Modelo de participación

Al escribir una consulta, opte por PLINQ invocando el ParallelEnumerable.AsParallel método de extensión en el origen de datos, 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($"{evenNums.Count()} even numbers out of {source.Count()} total");
// 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 AsParallel método de extensión enlaza los operadores de consulta posteriores, en este caso, where y select, a las System.Linq.ParallelEnumerable implementaciones.

Modos de ejecución

De forma predeterminada, PLINQ es conservador. En tiempo de ejecución, la infraestructura PLINQ analiza la estructura general de la consulta. Si es probable que la consulta produzca velocidades por paralelización, PLINQ divide la secuencia de origen en tareas que se pueden ejecutar simultáneamente. Si no es seguro paralelizar una consulta, PLINQ solo ejecuta la consulta secuencialmente. Si PLINQ tiene una opción entre un algoritmo paralelo potencialmente costoso o un algoritmo secuencial económico, elige el algoritmo secuencial de forma predeterminada. Puede usar el WithExecutionMode método y la System.Linq.ParallelExecutionMode enumeración para indicar a PLINQ que seleccione el algoritmo paralelo. Esto resulta útil cuando se sabe mediante la prueba y la medición de 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 forma predeterminada, PLINQ usa todos los procesadores del equipo host. Puede indicar a PLINQ que use no más de un número especificado de procesadores mediante el WithDegreeOfParallelism método . Esto es útil cuando desea asegurarse de que otros procesos que se ejecutan en el equipo reciben una cantidad determinada de tiempo de CPU. El fragmento de código siguiente limita la consulta para usar 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 los casos en los que una consulta realiza una cantidad significativa de trabajo no enlazado a proceso, como la E/S de archivo, puede ser beneficioso especificar un grado de paralelismo mayor que el número de núcleos en la máquina.

Consultas paralelas ordenadas versus 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 AsOrdered operador para este propósito. AsOrdered es distinto de AsSequential. Una AsOrdered secuencia se sigue procesando en paralelo, pero sus resultados se almacenan en búfer y se ordenan. Dado que la conservación del orden normalmente implica trabajo adicional, es posible que una AsOrdered secuencia se procese más lentamente que la secuencia predeterminada AsUnordered . Si una operación paralela ordenada determinada es 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 habilitar la conservación del 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 obtener más información, vea Conservación de pedidos en PLINQ.

Consultas paralelas frente a secuenciales

Algunas operaciones requieren que los datos de origen se entreguen de forma secuencial. Los ParallelEnumerable operadores de consulta vuelven al modo secuencial automáticamente cuando es necesario. Para los operadores de consulta definidos por el usuario y los delegados de usuario que requieren ejecución secuencial, PLINQ proporciona el AsSequential método . Cuando usas AsSequential, todos los operadores subsiguientes en la consulta se ejecutan secuencialmente hasta que se llame de nuevo 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 de cada subproceso de trabajo deben combinarse de nuevo en el subproceso principal para su consumo por un foreach bucle (For Each en Visual Basic) o insertarlos en una lista o matriz. En algunos casos, puede ser beneficioso especificar un tipo determinado de operación de combinación, por ejemplo, para empezar a generar resultados más rápidamente. Para ello, PLINQ admite el WithMergeOptions método y la ParallelMergeOptions enumeración . Para obtener más información, vea Opciones de combinación en PLINQ.

El operador ForAll

En las consultas LINQ secuenciales, la ejecución se aplaza hasta que la consulta se enumera en un foreach bucle (For Each en Visual Basic) o invocando un método como ToList , ToArray o ToDictionary. En PLINQ, también puede usar foreach para ejecutar la consulta y recorrer en iteración los resultados. Sin embargo, foreach sí mismo no se ejecuta en paralelo y, por lo tanto, requiere que la salida de todas las tareas paralelas se combine de nuevo en el subproceso en el que se ejecuta el bucle. En PLINQ, puede usar foreach cuando se debe conservar el orden final de los resultados de la consulta y también siempre que se procesen los resultados de forma serie, por ejemplo, cuando se llama Console.WriteLine a para cada elemento. Para una ejecución de consultas más rápida cuando no se requiere conservación del orden y cuando el procesamiento de los resultados se puede paralelizar, use el ForAll método para ejecutar una consulta PLINQ. ForAll no realiza este paso de combinación final. En el ejemplo de código siguiente se muestra cómo usar el ForAll método . System.Collections.Concurrent.ConcurrentBag<T> se usa aquí porque está optimizado para varios subprocesos que se agregan simultáneamente 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 consultas.

ForAll en comparación con ForEach

Cancelación

PLINQ se integra con los tipos de cancelación en .NET. (Para obtener más información, consulte Cancelación en subprocesos administrados). Por lo tanto, a diferencia de las consultas LINQ to Objects secuenciales, se pueden cancelar las consultas PLINQ. Para crear una consulta PLINQ cancelable, use el WithCancellation operador en la consulta y proporcione una CancellationToken instancia 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 continúe procesando algunos elementos después de establecer 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, es posible que se produzcan varias excepciones a partir de subprocesos diferentes simultáneamente. Además, el código para controlar la excepción podría estar en un subproceso diferente al código que produjo 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 escribiendo un particionador personalizado que aproveche algunas características de los datos de origen. En la consulta, el propio 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 un número fijo de particiones (aunque los datos se pueden reasignar dinámicamente a esas particiones durante el tiempo de ejecución para el equilibrio de carga). For y ForEach solo admiten la creación de particiones dinámicas, lo que significa que el número de particiones cambia en tiempo de ejecución. Para obtener más información, consulte Particionadores personalizados para PLINQ y TPL.

Medición del rendimiento de PLINQ

En muchos casos, una consulta se puede paralelizar, pero la sobrecarga de configurar la consulta paralela supera la ventaja de rendimiento obtenida. Si una consulta no realiza mucho cálculo o si el origen de datos es pequeño, una consulta PLINQ puede ser más lenta que una consulta LINQ to Objects secuencial. Puede usar el Analizador de rendimiento paralelo en Visual Studio Team Server para comparar el rendimiento de varias consultas, buscar cuellos de botella de procesamiento y determinar si la consulta se ejecuta en paralelo o secuencialmente. Para obtener más información, vea Visualizador de concurrencia y Cómo: Medición del rendimiento de consultas PLINQ.

Consulte también