Particionadores personalizados para PLINQ y TPLCustom Partitioners for PLINQ and TPL

Para paralelizar una operación en un origen de datos, uno de los pasos esenciales es particionar el origen en varias secciones a las que pueden acceder varios subprocesos al mismo tiempo.To parallelize an operation on a data source, one of the essential steps is to partition the source into multiple sections that can be accessed concurrently by multiple threads. PLINQ y la biblioteca TPL proporcionan particionadores predeterminados que funcionan de manera transparente al escribir un bucle ForEach o una consulta en paralelo.PLINQ and the Task Parallel Library (TPL) provide default partitioners that work transparently when you write a parallel query or ForEach loop. Para escenarios más avanzados, puede conectar su propio particionador.For more advanced scenarios, you can plug in your own partitioner.

Tipos de particionesKinds of Partitioning

Hay muchas maneras de particionar un origen de datos.There are many ways to partition a data source. En los enfoques más eficaces, varios subprocesos cooperan para procesar la secuencia de origen original, en lugar de separar físicamente el origen en varias secuencias secundarias.In the most efficient approaches, multiple threads cooperate to process the original source sequence, rather than physically separating the source into multiple subsequences. Para matrices y otros orígenes indexados como colecciones IList donde la longitud se conoce de antemano, la creación de particiones por rangos es el tipo más sencillo de creación de particiones.For arrays and other indexed sources such as IList collections where the length is known in advance, range partitioning is the simplest kind of partitioning. Cada subproceso recibe índices exclusivos de apertura y cierre, para poder procesar su rango del origen sin sobrescribir subprocesos ni ser sobrescrito por algún subproceso.Every thread receives unique beginning and ending indexes, so that it can process its range of the source without overwriting or being overwritten by any other thread. La única sobrecarga implicada en la creación de particiones por rangos es el trabajo inicial de crear los rangos; después de eso, no se requiere ninguna sincronización adicional.The only overhead involved in range partitioning is the initial work of creating the ranges; no additional synchronization is required after that. Por lo tanto, puede proporcionar un buen rendimiento siempre y cuando la carga de trabajo se divida de manera uniforme.Therefore, it can provide good performance as long as the workload is divided evenly. Una desventaja de la creación de particiones por rangos es que, si un subproceso finaliza de forma anticipada, no puede ayudar a los otros subprocesos a finalizar su trabajo.A disadvantage of range partitioning is that if one thread finishes early, it cannot help the other threads finish their work.

Para listas vinculadas u otras colecciones cuya longitud no se conoce, puede usar la creación de particiones por fragmentos.For linked lists or other collections whose length is not known, you can use chunk partitioning. En la creación de particiones por fragmentos, cada subproceso o tarea de una consulta o bucle paralelos consumen algunos elementos de origen de un fragmento, los procesan y vuelven a activarse para recuperar elementos adicionales.In chunk partitioning, every thread or task in a parallel loop or query consumes some number of source elements in one chunk, processes them, and then comes back to retrieve additional elements. El particionador garantiza que todos los elementos se distribuyan y que no se dupliquen.The partitioner ensures that all elements are distributed and that there are no duplicates. Un fragmento puede tener cualquier tamaño.A chunk may be any size. Por ejemplo, el particionador que se muestra en Cómo: Implementar las particiones dinámicas crea fragmentos que contienen un solo elemento.For example, the partitioner that is demonstrated in How to: Implement Dynamic Partitions creates chunks that contain just one element. Siempre que los fragmentos no sean demasiado grandes, este tipo de creación de particiones tiene un equilibrio de carga inherente, porque la asignación de elementos a los subprocesos no es predeterminada.As long as the chunks are not too large, this kind of partitioning is inherently load-balancing because the assignment of elements to threads is not pre-determined. Sin embargo, el particionador no incurre en sobrecarga de sincronización cada vez que el subproceso necesita obtener otro fragmento.However, the partitioner does incur the synchronization overhead each time the thread needs to get another chunk. La cantidad de sincronización en que se incurre en estos casos es inversamente proporcional al tamaño de los fragmentos.The amount of synchronization incurred in these cases is inversely proportional to the size of the chunks.

En general, la creación de particiones por rangos solo es más rápida cuando el tiempo de ejecución del delegado es de bajo a moderado y el origen tiene un gran número de elementos y el trabajo total de cada partición es más o menos equivalente.In general, range partitioning is only faster when the execution time of the delegate is small to moderate, and the source has a large number of elements, and the total work of each partition is roughly equivalent. Por tanto, la creación de particiones por fragmentos suele ser más rápida en la mayoría de los casos.Chunk partitioning is therefore generally faster in most cases. En los orígenes con un número reducido de elementos o con tiempos de ejecución más largos para el delegado, el rendimiento de la creación de particiones por fragmentos y rangos es prácticamente el mismo.On sources with a small number of elements or longer execution times for the delegate, then the performance of chunk and range partitioning is about equal.

Los particionadores de TPL también admiten un número de particiones dinámico.The TPL partitioners also support a dynamic number of partitions. Esto significa que se pueden crear particiones sobre la marcha, por ejemplo, cuando el bucle ForEach genera una nueva tarea.This means they can create partitions on-the-fly, for example, when the ForEach loop spawns a new task. Esta característica permite al particionador escalarse junto con el propio bucle.This feature enables the partitioner to scale together with the loop itself. Los particionadores dinámicos también tienen un equilibrio de carga inherente.Dynamic partitioners are also inherently load-balancing. Cuando se crea un particionador personalizado, debe admitir la creación de particiones dinámicas para poder usarlas desde un bucle ForEach.When you create a custom partitioner, you must support dynamic partitioning to be consumable from a ForEach loop.

Configuración de particionadores de equilibrio de carga para PLINQConfiguring Load Balancing Partitioners for PLINQ

Algunas sobrecargas del método Partitioner.Create permiten crear un particionador para una matriz o un origen IList y especificar si debe intentar equilibrar la carga de trabajo entre los subprocesos.Some overloads of the Partitioner.Create method let you create a partitioner for an array or IList source and specify whether it should attempt to balance the workload among the threads. Cuando se configura el particionador para equilibrar la carga, se emplea la creación de particiones por fragmentos, y los elementos se entregan a cada partición en pequeños fragmentos a medida que se solicitan.When the partitioner is configured to load-balance, chunk partitioning is used, and the elements are handed off to each partition in small chunks as they are requested. Este enfoque ayuda a garantizar que todas las particiones tienen elementos para procesar hasta que se completa totalmente un bucle o una consulta.This approach helps ensure that all partitions have elements to process until the entire loop or query is completed. Se puede usar una sobrecarga adicional para proporcionar particiones de equilibrio de carga de cualquier origen IEnumerable.An additional overload can be used to provide load-balancing partitioning of any IEnumerable source.

En general, el equilibrio de carga requiere que las particiones soliciten elementos con relativa frecuencia desde el particionador.In general, load balancing requires the partitions to request elements relatively frequently from the partitioner. Por el contrario, un particionador que crea particiones estáticas puede asignar todos los elementos a cada particionador al mismo tiempo mediante el uso de la creación de particiones por rangos o por fragmentos.By contrast, a partitioner that does static partitioning can assign the elements to each partitioner all at once by using either range or chunk partitioning. Esto requiere menos sobrecarga que el equilibrio de carga, pero es posible que tarde más tiempo en ejecutarse si un subproceso termina significativamente con más trabajo que los demás.This requires less overhead than load balancing, but it might take longer to execute if one thread ends up with significantly more work than the others. De forma predeterminada, cuando se pasa una interfaz IList o una matriz, PLINQ siempre utiliza la creación de particiones por rangos sin equilibrio de carga.By default when it is passed an IList or an array, PLINQ always uses range partitioning without load balancing. Para habilitar el equilibrio de carga para PLINQ, use el método Partitioner.Create, tal como se muestra en el ejemplo siguiente.To enable load balancing for PLINQ, use the Partitioner.Create method, as shown in the following example.

// Static partitioning requires indexable source. Load balancing
// can use any IEnumerable.
var nums = Enumerable.Range(0, 100000000).ToArray();

// Create a load-balancing partitioner. Or specify false for static partitioning.
Partitioner<int> customPartitioner = Partitioner.Create(nums, true);

// The partitioner is the query's data source.
var q = from x in customPartitioner.AsParallel()
        select x * Math.PI;

q.ForAll((x) =>
{
    ProcessData(x);
});
' Static number of partitions requires indexable source.
Dim nums = Enumerable.Range(0, 100000000).ToArray()

' Create a load-balancing partitioner. Or specify false For  Shared partitioning.
Dim customPartitioner = Partitioner.Create(nums, True)

' The partitioner is the query's data source.
Dim q = From x In customPartitioner.AsParallel()
        Select x * Math.PI

q.ForAll(Sub(x) ProcessData(x))

La mejor forma de determinar si usar el equilibrio de carga en un escenario concreto es experimentar y medir el tiempo que las operaciones tardan en completarse con cargas representativas y configuraciones de equipos.The best way to determine whether to use load balancing in any given scenario is to experiment and measure how long it takes operations to complete under representative loads and computer configurations. Por ejemplo, el particionamiento estático podría proporcionar un aumento significativo de la velocidad en un equipo de varios núcleos que tenga solo unos pocos núcleos, pero podría dar como resultado una ralentización de los equipos que tienen relativamente muchos núcleos.For example, static partitioning might provide significant speedup on a multi-core computer that has only a few cores, but it might result in slowdowns on computers that have relatively many cores.

En la siguiente tabla se enumeran las sobrecargas disponibles del método Create.The following table lists the available overloads of the Create method. Estos particionadores no están limitados para utilizarse solo con PLINQ o Task.These partitioners are not limited to use only with PLINQ or Task. También se pueden utilizar con cualquier construcción paralela personalizada.They can also be used with any custom parallel construct.

SobrecargaOverload Usa el equilibrio de cargaUses load balancing
Create<TSource>(IEnumerable<TSource>) SiempreAlways
Create<TSource>(TSource[], Boolean) Cuando el argumento Boolean se especifica como trueWhen the Boolean argument is specified as true
Create<TSource>(IList<TSource>, Boolean) Cuando el argumento Boolean se especifica como trueWhen the Boolean argument is specified as true
Create(Int32, Int32) NuncaNever
Create(Int32, Int32, Int32) NuncaNever
Create(Int64, Int64) NuncaNever
Create(Int64, Int64, Int64) NuncaNever

Configuración de particionadores por rangos estáticos para Parallel.ForEachConfiguring Static Range Partitioners for Parallel.ForEach

En un bucle For, el cuerpo del bucle se proporciona al método como un delegado.In a For loop, the body of the loop is provided to the method as a delegate. El costo de invocar ese delegado es aproximadamente el mismo que una llamada al método virtual.The cost of invoking that delegate is about the same as a virtual method call. En algunos escenarios, el cuerpo de un bucle paralelo podría ser lo suficientemente pequeño como para que el costo de la invocación del delegado en cada iteración del bucle sea significativo.In some scenarios, the body of a parallel loop might be small enough that the cost of the delegate invocation on each loop iteration becomes significant. En estas situaciones, puede usar una de las sobrecargas Create para crear una interfaz IEnumerable<T> de particiones por rangos de los elementos de origen.In such situations, you can use one of the Create overloads to create an IEnumerable<T> of range partitions over the source elements. A continuación, puede pasar esta colección de intervalos a un método ForEach cuyo cuerpo se compone de un bucle for normal.Then, you can pass this collection of ranges to a ForEach method whose body consists of a regular for loop. La ventaja de este enfoque es que se incurre en el costo de invocación del delegado solo una vez por rango, en lugar de una vez por elemento.The benefit of this approach is that the delegate invocation cost is incurred only once per range, rather than once per element. En el siguiente ejemplo se muestra el patrón básico.The following example demonstrates the basic pattern.

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {

        // Source must be array or IList.
        var source = Enumerable.Range(0, 100000).ToArray();

        // Partition the entire source array.
        var rangePartitioner = Partitioner.Create(0, source.Length);

        double[] results = new double[source.Length];

        // Loop over the partitions in parallel.
        Parallel.ForEach(rangePartitioner, (range, loopState) =>
        {
            // Loop over each range element without a delegate invocation.
            for (int i = range.Item1; i < range.Item2; i++)
            {
                results[i] = source[i] * Math.PI;
            }
        });

        Console.WriteLine("Operation complete. Print results? y/n");
        char input = Console.ReadKey().KeyChar;
        if (input == 'y' || input == 'Y')
        {
            foreach(double d in results)
            {
                Console.Write("{0} ", d);
            }           
        }
    }
}
Imports System.Threading.Tasks
Imports System.Collections.Concurrent

Module PartitionDemo

    Sub Main()
        ' Source must be array or IList.
        Dim source = Enumerable.Range(0, 100000).ToArray()

        ' Partition the entire source array. 
        ' Let the partitioner size the ranges.
        Dim rangePartitioner = Partitioner.Create(0, source.Length)

        Dim results(source.Length - 1) As Double

        ' Loop over the partitions in parallel. The Sub is invoked
        ' once per partition.
        Parallel.ForEach(rangePartitioner, Sub(range, loopState)

                                               ' Loop over each range element without a delegate invocation.
                                               For i As Integer = range.Item1 To range.Item2 - 1
                                                   results(i) = source(i) * Math.PI
                                               Next
                                           End Sub)
        Console.WriteLine("Operation complete. Print results? y/n")
        Dim input As Char = Console.ReadKey().KeyChar
        If input = "y"c Or input = "Y"c Then
            For Each d As Double In results
                Console.Write("{0} ", d)
            Next
        End If

    End Sub
End Module

Cada subproceso del bucle recibe su propio Tuple<T1,T2> que contiene los valores de índice de inicio y fin en el subrango especificado.Every thread in the loop receives its own Tuple<T1,T2> that contains the starting and ending index values in the specified sub-range. El bucle for interno usa los valores fromInclusive y toExclusive para recorrer en bucle la matriz o IList directamente.The inner for loop uses the fromInclusive and toExclusive values to loop over the array or the IList directly.

Una de las sobrecargas Create le permite especificar el tamaño de las particiones y el número de particiones.One of the Create overloads lets you specify the size of the partitions, and the number of partitions. Esta sobrecarga puede usarse en escenarios en los que el trabajo por elemento es tan bajo que incluso una llamada a un método virtual por elemento tiene un impacto considerable en el rendimiento.This overload can be used in scenarios where the work per element is so low that even one virtual method call per element has a noticeable impact on performance.

Particionadores personalizadosCustom Partitioners

En algunos casos, podría merecer la pena o incluso ser necesario implementar un particionador propio.In some scenarios, it might be worthwhile or even required to implement your own partitioner. Por ejemplo, podría tener una clase de colección personalizada que puede crear particiones de forma más eficaz que los particionadores predeterminados, según su conocimiento de la estructura interna de la clase.For example, you might have a custom collection class that you can partition more efficiently than the default partitioners can, based on your knowledge of the internal structure of the class. O bien, puede que desee crear particiones por rangos de tamaños diferentes basándose en su conocimiento de cuánto tiempo se tardará en procesar los elementos en ubicaciones distintas de la colección de origen.Or, you may want to create range partitions of varying sizes based on your knowledge of how long it will take to process elements at different locations in the source collection.

Para crear un particionador personalizado básico, derive una clase de System.Collections.Concurrent.Partitioner<TSource> e invalide los métodos virtuales, tal como se describe en la tabla siguiente.To create a basic custom partitioner, derive a class from System.Collections.Concurrent.Partitioner<TSource> and override the virtual methods, as described in the following table.

GetPartitions El subproceso principal llama una vez a este método y se devuelve IList(IEnumerator(TSource)).This method is called once by the main thread and returns an IList(IEnumerator(TSource)). Cada subproceso de trabajo del bucle o la consulta puede llamar a GetEnumerator en la lista para recuperar IEnumerator<T> a través de una partición distinta.Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator<T> over a distinct partition.
SupportsDynamicPartitions Devuelva true si implementa GetDynamicPartitions; en caso contrario, false.Return true if you implement GetDynamicPartitions, otherwise, false.
GetDynamicPartitions Si SupportsDynamicPartitions es true, opcionalmente, se puede llamar a este método en lugar de a GetPartitions.If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.

Si los resultados se pueden ordenar o precisa de acceso indexado a los elementos, realice una derivación de System.Collections.Concurrent.OrderablePartitioner<TSource> e invalide los métodos virtuales como se describe en la tabla siguiente.If the results must be sortable or you require indexed access into the elements, then derive from System.Collections.Concurrent.OrderablePartitioner<TSource> and override its virtual methods as described in the following table.

GetPartitions El subproceso principal llama una vez a este método y se devuelve IList(IEnumerator(TSource)).This method is called once by the main thread and returns an IList(IEnumerator(TSource)). Cada subproceso de trabajo del bucle o la consulta puede llamar a GetEnumerator en la lista para recuperar IEnumerator<T> a través de una partición distinta.Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator<T> over a distinct partition.
SupportsDynamicPartitions Devuelva true si implementa GetDynamicPartitions; de lo contrario, false.Return true if you implement GetDynamicPartitions; otherwise, false.
GetDynamicPartitions Normalmente, esto simplemente llama a GetOrderableDynamicPartitions.Typically, this just calls GetOrderableDynamicPartitions.
GetOrderableDynamicPartitions Si SupportsDynamicPartitions es true, opcionalmente, se puede llamar a este método en lugar de a GetPartitions.If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.

En la tabla siguiente se proporcionan detalles adicionales sobre cómo los tres tipos de particionadores de equilibrio de carga implementan la clase OrderablePartitioner<TSource>.The following table provides additional details about how the three kinds of load-balancing partitioners implement the OrderablePartitioner<TSource> class.

Método/propiedadMethod/Property IList/matriz sin equilibrio de cargaIList / Array without Load Balancing IList/matriz con equilibrio de cargaIList / Array with Load Balancing IEnumerableIEnumerable
GetOrderablePartitions Usa la creación de particiones por rangosUses range partitioning Usa la creación de particiones por fragmentos optimizada para listas de la clase partitionCount especificadaUses chunk partitioning optimized for Lists for the partitionCount specified Usa la creación de particiones por fragmentos mediante la creación de un número estático de particiones.Uses chunk partitioning by creating a static number of partitions.
OrderablePartitioner<TSource>.GetOrderableDynamicPartitions Genera una excepción no admitidaThrows not-supported exception Usa la creación de particiones por fragmentos para listas y particiones dinámicasUses chunk partitioning optimized for Lists and dynamic partitions Usa la creación de particiones por fragmentos mediante la creación de un número dinámico de particiones.Uses chunk partitioning by creating a dynamic number of partitions.
KeysOrderedInEachPartition Devuelve trueReturns true Devuelve trueReturns true Devuelve trueReturns true
KeysOrderedAcrossPartitions Devuelve trueReturns true Devuelve falseReturns false Devuelve falseReturns false
KeysNormalized Devuelve trueReturns true Devuelve trueReturns true Devuelve trueReturns true
SupportsDynamicPartitions Devuelve falseReturns false Devuelve trueReturns true Devuelve trueReturns true

Particiones dinámicasDynamic Partitions

Si pretende que el particionador se use en un método ForEach, debe tener la capacidad de devolver un número dinámico de particiones.If you intend the partitioner to be used in a ForEach method, you must be able to return a dynamic number of partitions. Esto significa que el particionador puede proporcionar un enumerador para una nueva partición a petición en cualquier momento durante la ejecución del bucle.This means that the partitioner can supply an enumerator for a new partition on-demand at any time during loop execution. Básicamente, siempre que el bucle agrega una nueva tarea en paralelo, solicita una nueva partición de esa tarea.Basically, whenever the loop adds a new parallel task, it requests a new partition for that task. Si necesita que los datos se puedan ordenar, realice la derivación de System.Collections.Concurrent.OrderablePartitioner<TSource>, para que a cada elemento de cada partición se le asigne un índice único.If you require the data to be orderable, then derive from System.Collections.Concurrent.OrderablePartitioner<TSource> so that each item in each partition is assigned a unique index.

Para obtener más información y un ejemplo, vea Cómo: Implementar las particiones dinámicas.For more information, and an example, see How to: Implement Dynamic Partitions.

Contrato para particionadoresContract for Partitioners

Al implementar un particionador personalizado, siga estas instrucciones para ayudar a garantizar la interacción correcta con PLINQ y ForEach en la biblioteca TPL:When you implement a custom partitioner, follow these guidelines to help ensure correct interaction with PLINQ and ForEach in the TPL:

  • Si se llama a GetPartitions con un argumento de cero o menos para partitionsCount, se produce ArgumentOutOfRangeException.If GetPartitions is called with an argument of zero or less for partitionsCount, throw ArgumentOutOfRangeException. Aunque PLINQ y TPL nunca pasarán una clase partitionCount igual a 0, no obstante, se recomienda adoptar medidas preventivas para evitar esta posibilidad.Although PLINQ and TPL will never pass in a partitionCount equal to 0, we nevertheless recommend that you guard against the possibility.

  • GetPartitions y GetOrderablePartitions siempre deben devolver una serie de particiones partitionsCount.GetPartitions and GetOrderablePartitions should always return partitionsCount number of partitions. Si el particionador agota los datos y no puede crear tantas particiones como se han solicitado, el método debe devolver un enumerador vacío para cada una de las particiones restantes.If the partitioner runs out of data and cannot create as many partitions as requested, then the method should return an empty enumerator for each of the remaining partitions. De lo contrario, PLINQ y TPL producirán una excepción InvalidOperationException.Otherwise, both PLINQ and TPL will throw an InvalidOperationException.

  • GetPartitions, GetOrderablePartitions, GetDynamicPartitions y GetOrderableDynamicPartitions nunca deberían devolver null (Nothing en Visual Basic).GetPartitions, GetOrderablePartitions, GetDynamicPartitions, and GetOrderableDynamicPartitions should never return null (Nothing in Visual Basic). Si lo hacen, PLINQ/TPL producirán una excepción InvalidOperationException.If they do, PLINQ / TPL will throw an InvalidOperationException.

  • Los métodos que devuelven particiones siempre deberían devolver particiones que puedan enumerar completamente y de forma única el origen de datos.Methods that return partitions should always return partitions that can fully and uniquely enumerate the data source. No debería haber ninguna duplicación del origen de datos ni elementos omitidos a menos que lo requiera específicamente el diseño del particionador.There should be no duplication in the data source or skipped items unless specifically required by the design of the partitioner. Si no se respeta esta regla, se puede alterar el orden de salida.If this rule is not followed, then the output order may be scrambled.

  • Los siguientes captadores booleanos deben devolver siempre con precisión los siguientes valores para que no se altere el orden de salida:The following Boolean getters must always accurately return the following values so that the output order is not scrambled:

    • KeysOrderedInEachPartition: cada partición devuelve elementos con índices de clave crecientes.KeysOrderedInEachPartition: Each partition returns elements with increasing key indices.

    • KeysOrderedAcrossPartitions: para todas las particiones que se devuelven, los índices de clave de la partición i son más altos que los de la partición i-1.KeysOrderedAcrossPartitions: For all partitions that are returned, the key indices in partition i are higher than the key indices in partition i-1.

    • KeysNormalized: todos los índices de clave aumentan ininterrumpidamente sin espacios, empezando desde cero.KeysNormalized: All key indices are monotonically increasing without gaps, starting from zero.

  • Todos los índices deben ser únicos.All indices must be unique. No puede haber índices duplicados.There may not be duplicate indices. Si no se respeta esta regla, se puede alterar el orden de salida.If this rule is not followed, then the output order may be scrambled.

  • Todos los índices deben ser no negativos.All indices must be nonnegative. Si no se respeta esta regla, PLINQ/TPL pueden producir excepciones.If this rule is not followed, then PLINQ/TPL may throw exceptions.

Vea tambiénSee also