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.
¿Qué es una consulta paralela?
Language-Integrated Query (LINQ) apareció por primera vez en .NET Framework 3.0. Representa un modelo unificado para consultar cualquier origen de datos System.Collections.IEnumerable o System.Collections.Generic.IEnumerable<T> con seguridad de tipos. LINQ to Objects es el nombre para los consultas LINQ que se ejecutan con las colecciones en memoria, como List<T>, y las matrices. En este artículo se supone que tiene un conocimiento básico de LINQ. Para obtener más información, vea LINQ (Language-Integrated Query).
Parallel LINQ (PLINQ) es una implementación paralela del modelo LINQ. Una consulta PLINQ se parece de muchas maneras a una consulta LINQ to Objects no paralela. Las consultas PLINQ, al igual que las consultas LINQ secuenciales, funcionan con cualquier origen de datos IEnumerable o IEnumerable<T> en memoria y usan la ejecución diferida, lo que significa que no empiezan a ejecutarse hasta que se enumere la consulta. La diferencia primaria es que PLINQ intenta realizar el uso completo de todos los procesadores en el sistema. Para hacer esto, divide 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 bastante más rápidamente.
A través de la ejecución en paralelo, PLINQ puede lograr mejoras de rendimiento significativas respecto al código heredado para ciertos tipos de consultas, a menudo con solo 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ápidamente en PLINQ. De hecho, la ejecución en paralelo realmente reduce la velocidad de ciertas consultas. Por consiguiente, debería entender en qué grado afectan a las consultas en paralelo ciertos aspectos como la ordenación. Para obtener más información, vea Introducción a la velocidad en PLINQ.
![]() |
---|
En esta documentación, se utilizan expresiones lambda para definir delegados en la PLINQ.Si no está familiarizado con las expresiones lambda en C# o Visual Basic, vea Expresiones lambda en PLINQ y TPL. |
El resto de este artículo proporciona información general de las clases PLINQ principales y describe cómo crear las consultas PLINQ. Cada sección contiene vínculos a ejemplos de código e información más detallada.
La clase ParallelEnumerable
La clase System.Linq.ParallelEnumerable expone casi toda la funcionalidad de PLINQ. Esta clase y el resto de tipos del espacio 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 el 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 ejecutar cada una en paralelo. Si no está familiarizado con LINQ, vea Introducción a LINQ.
Además de los operadores de consulta estándar, la clase ParallelEnumerable contiene un conjunto de métodos que habilitan los comportamientos específicos de la ejecución en paralelo. Estos métodos específicos de PLINQ se muestran en la siguiente tabla.
Operador ParallelEnumerable |
Descripción |
---|---|
Punto de entrada para PLINQ. Especifica que el resto de la consulta se debería ejecutar en paralelo, si es posible. |
|
Especifica que el resto de la consulta se debería ejecutar secuencialmente, como un consulta LINQ no paralela. |
|
Especifica que PLINQ debería conservar la clasificación de la secuencia de origen para el resto de la consulta, o hasta que se cambie la clasificación, por ejemplo mediante una cláusula orderby (Order By en Visual Basic). |
|
Especifica que no es necesario que PLINQ conserve la clasificación de la secuencia de origen durante el resto de la consulta. |
|
Especifica que PLINQ debería supervisar periódicamente el estado del token de cancelación proporcionado y cancelar la ejecución si se solicita. |
|
Especifica el número máximo de procesadores que PLINQ debería usar para ejecutar la consulta en paralelo. |
|
Proporciona una sugerencia sobre cómo PLINQ debe, si es posible, volver a combinar los resultados paralelos en solo una secuencia en el subproceso utilizado. |
|
Especifica si PLINQ debería ejecutar la consulta en paralelo incluso cuando el comportamiento predeterminado sería ejecutarla secuencialmente. |
|
Un método de enumeración multiproceso que, a diferencia de iterar por los resultados de la consulta, permite volver a procesar los resultados en paralelo sin volver a combinar primero el subproceso de consumidor. |
|
Sobrecarga Aggregate |
Una sobrecarga que es exclusiva de PLINQ y habilita la agregación intermedia sobre las particiones locales de subprocesos, más una última función de agregación para combinar los resultados de todas las particiones. |
El modelo de opción
Al escribir una consulta, elija PLINQ invocando el método de extensión ParallelEnumerable.AsParallel en el origen de datos, como se muestra en el siguiente ejemplo.
Dim source = Enumerable.Range(1, 10000)
' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
Where Compute(num) > 0
Select num
var source = Enumerable.Range(1, 10000);
// Opt-in to PLINQ with AsParallel
var evenNums = from num in source.AsParallel()
where Compute(num) > 0
select num;
El método de extensión AsParallel enlaza a los operadores de consulta subsiguientes, en este caso, where y select, a las implementaciones de System.Linq.ParallelEnumerable.
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 aumentos de velocidad por la ejecución en paralelo, PLINQ divide la secuencia de origen en tareas que se pueden ejecutar simultáneamente. Si no es seguro para ejecutar una consulta en paralelo, PLINQ simplemente ejecuta la consulta de forma secuencial. Si PLINQ puede elegir entre un algoritmo paralelo potencialmente costoso o un algoritmo secuencial poco costoso, elige el algoritmo secuencial de forma predeterminada. Puede usar el método WithExecutionMode<TSource> y la enumeración System.Linq.ParallelExecutionMode para indicar a PLINQ que seleccione el algoritmo paralelo. Esto resulta útil cuando sabe por las pruebas y mediciones que una consulta determinada se ejecuta más rápidamente 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 en el equipo host hasta un máximo de 64. Puede indicar a PLINQ que no utilice más que un número especificado de procesadores usando el método WithDegreeOfParallelism<TSource>. Esto resulta útil si desea asegurarse de que los otros procesos que se ejecutan en el equipo reciben cierta cantidad de tiempo de CPU. El siguiente fragmento de código limita la consulta a usar un máximo de dos procesadores.
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
Where Compute(item) > 42
Select item
var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
where Compute(item) > 42
select item;
En los casos donde una consulta está realizando una cantidad significativa de trabajo enlazado sin cálculo, como la E/S de archivo, podría ser beneficioso especificar un grado de paralelismo mayor que el número de núcleos del equipo.
Consultar en paralelo ordenadas frente a no ordenadas
En algunas consultas, un operador de consulta debe generar resultados que conservan el orden de la secuencia de origen. PLINQ proporciona el operador AsOrdered para este propósito. AsOrdered es distinto de AsSequential<TSource>. Una secuencia AsOrdered se sigue procesando en paralelo, pero sus resultados se almacenan en búfer y se ordenan. Dado que la preservación del orden implica normalmente trabajo adicional, una secuencia AsOrdered se podría procesar más despacio que la secuencia AsUnordered<TSource> predeterminada. El hecho de que una operación en paralelo ordenada especial sea más rápida que una versión secuencial de la operación depende de muchos factores.
El siguiente ejemplo de código muestra las opciones que se deben elegir para conservar el orden.
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
Where num Mod 2 = 0
Select num
evenNums = from num in numbers.AsParallel().AsOrdered()
where num % 2 == 0
select num;
Para obtener más información, vea Conservar el orden en PLINQ.
Consultas enparalelo frente a secuenciales
Algunas operaciones requieren que los datos de origen se entreguen de una manera secuencial. Los operadores de consulta ParallelEnumerable revierten 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<TSource>. Al usar AsSequential<TSource>, se ejecutan secuencialmente todos los operadores subsiguientes en la consulta 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 los resultados de la consulta
Cuando una consulta PLINQ se ejecuta en paralelo, sus resultados de cada subproceso de trabajo deben volver a combinarse en el subproceso principal para su uso en un bucle foreach (For Each en Visual Basic) o la inserción en una lista o matriz. En algunos casos, podría ser beneficioso especificar un tipo determinado de operación de combinación, por ejemplo, para empezar a generar resultados más rápidamente. Para este propósito, PLINQ admite el método WithMergeOptions<TSource> y la enumeración ParallelMergeOptions. Para obtener más información, vea Opciones de combinación en PLINQ.
El operador ForAll
En las consultas secuenciales de LINQ, la ejecución se difiere hasta que la consulta se enumere en un bucle foreach (For Each en Visual Basic) o al invocar un método como ToList<TSource>, ToTSource> o ToDictionary. En PLINQ, también se puede usar foreach para ejecutar la consulta e iterar por los resultados. Sin embargo, el propio bucle foreach no se ejecuta en paralelo y, por consiguiente, requiere que el resultado de todas las tareas paralelas se vuelva a combinar en el subproceso en el que se está ejecutando el bucle. En PLINQ, puede usar foreach cuando deba conservar el orden final de los resultados de la consulta, y también cada vez que procese los resultados en serie, por ejemplo cuando está llamando a Console.WriteLine para cada elemento. Para una ejecución más rápida de la consulta cuando no se requiere la conservación del orden y cuando el propio procesamiento de los resultados se puede ejecutar en paralelo, use el método ForAll<TSource> para ejecutar una consulta PLINQ. ForAll<TSource> no realiza este paso final de la combinación. En el siguiente ejemplo de código, se muestra cómo utilizar el método ForAll<TSource>. Se usa System.Collections.Concurrent.ConcurrentBag<T> en este caso porque está optimizado para varios subprocesos que agregan elementos simultáneamente sin intentar quitar ningún elemento.
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)))
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)));
En la siguiente ilustración, se muestra la diferencia entre foreach y ForAll<TSource> con respecto a la ejecución de una consulta.
Cancelación
PLINQ se integra con los tipos de cancelación en .NET Framework 4. (Para obtener más información, vea Cancelación). Por consiguiente, a diferencia de las consultas secuenciales LINQ to Objects, las consultas PLINQ pueden cancelarse. Crear una consulta PLINQ cancelable, use el operador WithCancellation<TSource> en la consulta y proporcione una instancia de CancellationToken como argumento. Cuando se establece en true la propiedad IsCancellationRequested del token, PLINQ lo observará, detendrá el procesamiento en todos los subprocesos y generará una excepción OperationCanceledException.
Es posible que una consulta PLINQ continúe procesando algunos elementos una vez establecido el token de cancelación.
Para lograr una sensibilidad mayor, puede responder también a las solicitudes de cancelación en los 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, se podrían producir simultáneamente varias excepciones de diferentes subprocesos. Asimismo, el código para controlar la excepción podría encontrarse un subproceso diferente al del código que produjo la excepción. PLINQ usa el tipo AggregateException para encapsular todas las excepciones producidas por una consulta y vuelve a calcular las referencias de esas excepciones para el subproceso que realiza la llamada. En el subproceso que realiza la llamada, solo se requiere un bloque try-catch. Sin embargo, puede iterar por todas las excepciones encapsuladas en AggregateException y detectar cualquiera de la que se pueda recuperar de forma segura. En casos raros, se pueden producir algunas excepciones que no se encapsulan en AggregateException y los objetos ThreadAbortException tampoco se encapsulan.
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 ciertos casos, puede mejorar el rendimiento de las consultas si escribe un particionador personalizado que se aprovecha de alguna característica de los datos de origen. En la consulta, el propio particionador personalizado es el objeto enumerable que se consulta.
[Visual Basic]
Dim arr(10000) As Integer
Dim partitioner = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))
[C#]
int[] arr= ...;
Partitioner<int> partitioner = newMyArrayPartitioner<int>(arr);
var q = partitioner.AsParallel().Select(x => SomeFunction(x));
PLINQ admite un número de particiones fijo (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 dinámica de particiones, lo que significa que el número de particiones cambia en tiempo de ejecución. Para obtener más información, vea Particionadores personalizados para PLINQ y TPL.
Medir el rendimiento de PLINQ
En muchos casos, una consulta se puede ejecutar en paralelo, pero la sobrecarga de preparar la consulta paralela supera a la ventaja de rendimiento obtenida. Si una consulta no realiza muchos cálculos o si el origen de datos es pequeño, una consulta PLINQ puede ser más lenta que una consulta secuencial LINQ to Objects. Puede usar el Analizador de rendimiento de procesamiento paralelo en Visual Studio Team Server para comparar el rendimiento de varias consultas, buscar los cuellos de botella del procesamiento y determinar si su consulta se está ejecutando en paralelo o secuencialmente. Para obtener más información, vea Visualizador de simultaneidad y Cómo: Medir el rendimiento de consultas PLINQ.