Share via


Partitioners personalizados para PLINQ e TPL

Em paralelo a uma operação em uma fonte de dados, uma das etapas essenciais é partição a fonte em várias seções que pode ser acessada simultaneamente por vários segmentos. Partitioners padrão que funcionam de forma transparente quando você escreve uma consulta paralela de fornecer PLINQ e a tarefa paralela TPL (biblioteca) ou ForEach loop. Para cenários mais avançados, você pode conectar seu próprio partitioner.

Tipos de particionamento

Há várias maneiras para particionar uma fonte de dados. Abordagens mais eficiente, vários threads cooperam para a seqüência de origem original do processo, em vez de separar fisicamente a fonte em várias subseqüências. Para conjuntos e outras fontes indexadas, como IList coleções onde o comprimento é conhecido antecipadamente, o particionamento do intervalo é o tipo mais simples de particionamento. Cada thread recebe começando e terminando de índices, exclusivo para que ele possa processar o seu intervalo de origem sem sobrescrever ou sendo substituído por qualquer outro segmento. Somente sobrecarga envolvida no intervalo de particionamento é o trabalho inicial de criação de intervalos; Nenhuma sincronização adicional é necessária depois disso. Portanto, ele pode fornecer um bom desempenho, desde que a carga de trabalho é dividida igualmente. Uma desvantagem do intervalo de particionamento é que se um thread terminar mais cedo, ele não pode ajudar outros threads concluir seu trabalho.

Para listas vinculadas ou outras coleções, cujo comprimento não for conhecido, você pode usar o particionamento do bloco. No bloco de particionamento, cada thread ou tarefa em uma consulta ou um loop paralelo consome algum número de elementos de origem em um bloco, processa e volta para recuperar os elementos adicionais. O partitioner garante que todos os elementos são distribuídos e que não haja duplicatas. Um bloco pode ser de qualquer tamanho. Por exemplo, o que é demonstrada partitioner Como: Implementar as partições dinâmicas cria blocos que contêm apenas um elemento. Como as partes não são muito grandes, esse tipo de particionamento é inerentemente balanceamento de carga porque a atribuição de elementos de segmentos não é predeterminada. No entanto, o partitioner incorrer a sobrecarga de sincronização sempre que o segmento precisa obter outro bloco. A quantidade de sincronização contraída nesses casos é inversamente proporcional ao tamanho do que as partes.

Em geral, a intervalo de particionamento só é mais rápido quando o tempo de execução do delegado é pequeno para médio e a fonte tem um grande número de elementos e o trabalho total de cada partição é aproximadamente equivalente. Particionamento de bloco, portanto, é geralmente mais rápido na maioria dos casos. Em fontes com um pequeno número de elementos ou mais tempos de execução para o delegado, em seguida, o desempenho do bloco e o intervalo de particionamento é sobre igual.

Partitioners a TPL também oferecem suporte a um número dinâmico das partições. Isso significa que eles podem criar partições em interrupções, por exemplo, quando o ForEach loop gera uma nova tarefa. Este recurso permite a partitioner expandir com o próprio loop. Partitioners dinâmicos também são inerentemente balanceamento de carga. Quando você cria um partitioner personalizado, você deve oferecer suporte ao particionamento dinâmico de ser consumíveis de um ForEach loop.

Configurando Partitioners para PLINQ de balanceamento de carga

Algumas sobrecargas da Partitioner.Create método permitem que você criar um partitioner para uma matriz ou IList de origem e especificar se ele deve tentar equilibrar a carga de trabalho entre segmentos. Quando o partitioner está configurado para o balanceamento de carga, particionamento de bloco é usado e os elementos são entregues a cada partição em pequenas partes conforme forem solicitadas. Essa abordagem ajuda a garantir que todas as partições possuem elementos para processar até que o loop inteiro ou a consulta é concluída. Uma sobrecarga adicional pode ser usada para fornecer balanceamento de carga de particionamento de qualquer IEnumerable de origem.

Em geral, o balanceamento de carga requer as partições para solicitar elementos relativamente freqüentemente o partitioner. Por outro lado, um partitioner que faz o particionamento estático pode atribuir os elementos a cada partitioner ao mesmo tempo usando o intervalo ou bloco de particionamento. Isso requer menos sobrecarga de balanceamento de carga, mas talvez demore mais a ser executado se um segmento termina com um trabalho muito mais que as outras. Por padrão quando ele é passado IList ou uma matriz, PLINQ sempre usa particionamento da faixa sem balanceamento de carga. Para habilitar o balanceamento de carga para PLINQ, use o Partitioner.Create método, conforme mostrado no exemplo a seguir.

        ' 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))

            // 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);
            });

A melhor maneira de determinar se deve usar o balanceamento de carga em qualquer determinado cenário é testá-las e medir o tempo sejam concluídas em cargas de representantes e as configurações do computador. Por exemplo, particionamento estático pode fornecer considerável aumento de velocidade em um computador com vários núcleos que tem apenas poucos núcleos, mas ele pode resultar em gargalos em computadores que possuem relativamente vários núcleos.

A seguinte tabela lista os métodos sobrecarregados de disponíveis a Create método. Essas partitioners não estão limitados a usar somente com PLINQ ou ForEach. Eles também podem ser usados com qualquer construção paralela personalizada.

Sobrecarga

Usa o balanceamento de carga

Create<TSource>(IEnumerable<TSource>)

Sempre

Create<TSource[](TSource>, Boolean)

Quando o argumento booleano é especificado como verdadeiro

Create<TSource>(IList<TSource>, Boolean)

Quando o argumento booleano é especificado como verdadeiro

Create(Int32, Int32)

Nunca

Create(Int32, Int32, Int32)

Nunca

Create(Int64, Int64)

Nunca

Create(Int64, Int64, Int64)

Nunca

Configurando a Partitioners de intervalo de estático para Parallel

Em um For loop, o corpo do loop é fornecido para o método como um delegado. O custo de invocar esse delegado é aproximadamente igual uma chamada de método virtual. Em alguns cenários, o corpo do loop paralelo pode ser pequeno o suficiente para que o custo de invocação de delegado em cada iteração do loop se torna significativo. Em tais situações, você pode usar um da Create sobrecargas para criar um IEnumerable<T> de partições de intervalo sobre elementos de origem. Em seguida, você pode passar essa coleção de intervalos para uma ForEach método cujo corpo consiste em uma expressão for loop. A vantagem dessa abordagem é que o custo de invocação de delegado é incorrido apenas uma vez por intervalo, em vez de uma vez por elemento. O exemplo a seguir demonstra o padrão básico.

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
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);
            }           
        }
    }
}

Todos os threads no loop recebe sua própria Tuple<T1, T2> que contém os valores de índice inicial e final no subintervalo especificado. Interno for usa um loop de fromInclusive e toExclusive valores para fazer um loop sobre a matriz ou o IList diretamente.

Dentre as Create sobrecargas permite que você especifique o tamanho das partições e o número de partições. Essa sobrecarga pode ser usada em cenários onde o trabalho por elemento é tão baixo que a chamada de um método virtual por elemento tem um impacto perceptível no desempenho.

Partitioners personalizadas

Em alguns cenários, talvez valha a pena ou mesmo necessárias para implementar seu próprio partitioner. Por exemplo, você pode ter uma classe de coleção personalizada, você pode criar a partição com mais eficiência do que a lata de partitioners padrão, com base em dados de Conhecimento da estrutura interna da classe. Ou, talvez você queira criar partições de intervalo de tamanhos variados, com base no seu conhecimento de quanto tempo levará para elementos do processo em diferentes locais da coleção de origem.

Para criar um partitioner personalizado básico, derivar uma classe de System.Collections.Concurrent.Partitioner<TSource> e substituir métodos virtuais, conforme descrito na tabela a seguir.

GetPartitions

Este método é chamado uma vez pelo thread principal e retorna um IList(IEnumerator(TSource)). Cada thread de trabalho no loop ou consulta pode chamar GetEnumerator na lista para recuperar um IEnumerator<T> em uma partição distinta.

SupportsDynamicPartitions

Retornar true se você implementar GetDynamicPartitions, caso contrário, false.

GetDynamicPartitions

Se SupportsDynamicPartitions é true, este método opcionalmente pode ser chamado em vez de GetPartitions.

Se os resultados devem ser classificáveis ou exigir o acesso indexado nos elementos, derivam de System.Collections.Concurrent.OrderablePartitioner<TSource> e substituir seus métodos virtuais, conforme descrito na tabela a seguir.

GetPartitions

Esse método é chamado uma vez pelo thread principal e retorna um IList(IEnumerator(TSource)). Cada thread de trabalho no loop ou consulta pode chamar GetEnumerator na lista para recuperar um IEnumerator<T> em uma partição distinta.

SupportsDynamicPartitions

Retornar true se você implementar GetDynamicPartitions; Caso contrário, false.

GetDynamicPartitions

Normalmente, isso simplesmente chama GetOrderableDynamicPartitions.

GetOrderableDynamicPartitions

Se SupportsDynamicPartitions é true, este método opcionalmente pode ser chamado em vez de GetPartitions.

A tabela a seguir fornece detalhes adicionais sobre como os três tipos de implementação de partitioners de balanceamento de carga de OrderablePartitioner<TSource> classe.

Método ou propriedade.

IList / Array sem balanceamento de carga

IList / Array com balanceamento de carga

IEnumerable

GetOrderablePartitions

Usa o intervalo de particionamento

Particionamento de fragmento usa otimizado para listas para o partitionCount especificado

Usa particionamento do bloco, criando um estático número de partições.

OrderablePartitioner<TSource>.GetOrderableDynamicPartitions

Exceção de lança não suportada

Usa divida particionamento otimizado para listas e as partições dinâmicas

Usa particionamento do bloco, criando um número dinâmico das partições.

KeysOrderedInEachPartition

Retorna true

Retorna true

Retorna true

KeysOrderedAcrossPartitions

Retorna true

Retorna false

Retorna false

KeysNormalized

Retorna true

Retorna true

Retorna true

SupportsDynamicPartitions

Retorna false

Retorna true

Retorna true

Partições dinâmicas

Se pretender que o partitioner para ser usado em um ForEach método, você deve ser capaz de retornar um número dinâmico das partições. Isso significa que o partitioner pode fornecer um enumerador para uma nova partição sob demanda a qualquer momento durante a execução do loop. Basicamente, sempre que o loop adiciona uma nova tarefa paralela, ele solicita uma nova partição para a tarefa. Se você exigir que os dados a serem solicitados, derivam de System.Collections.Concurrent.OrderablePartitioner<TSource> para que cada item em cada partição é atribuído um índice exclusivo.

Para obter mais informações e um exemplo, consulte Como: Implementar as partições dinâmicas.

Contrato para Partitioners

Quando você implementa um partitioner personalizado, siga estas diretrizes para ajudar a garantir a correta interação com o PLINQ e ForEach na TPL:

  • Se GetPartitions é chamado com um argumento de zero ou menos para partitionsCount, lance ArgumentOutOfRangeException. Embora o PLINQ e TPL nunca transmitirá um partitionCount igual a 0, não obstante recomendamos que você proteja-se contra a possibilidade.

  • GetPartitionse GetOrderablePartitions deve retornar sempre partitionsCount número de partições. Se o partitioner é executado fora dos dados e não pode criar quantas partições conforme solicitado, o método deve retornar um enumerador vazio para cada um dos demais partições. Caso contrário, PLINQ e a TPL lançarão uma InvalidOperationException.

  • GetPartitions, GetOrderablePartitions, GetDynamicPartitions, e GetOrderableDynamicPartitions nunca devem retornar null (Nothing em Visual Basic). Se o fizerem, PLINQ / TPL lançará um InvalidOperationException.

  • Métodos que retornam partições sempre devem retornar partições podem exclusivamente e totalmente enumerar a fonte de dados. Não deve haver nenhuma duplicação na fonte de dados ou itens ignorados, a menos que especificamente necessário para o design do partitioner. Se essa regra não é seguida, a ordem de saída pode embaralhada.

  • Os seguintes getters booleanos sempre com precisão devem retornar os seguintes valores para que a ordem de saída não está embaralhada:

    • KeysOrderedInEachPartition: Cada partição retorna os elementos com o aumento de índices de chaves.

    • KeysOrderedAcrossPartitions: Para todas as partições são retornados, os índices de chaves na partição i são maiores que os índices de chaves na partição i-1.

    • KeysNormalized: Todos os índices de chaves monotônica sem intervalos, começando com zero.

  • Todos os índices devem ser exclusivos. Talvez não haja índices duplicados. Se essa regra não é seguida, a ordem de saída pode embaralhada.

  • Todos os índices devem ser não negativos. Se essa regra não for respeitada, o PLINQ/TPL pode lançar exceções.

Consulte também

Tarefas

Como: Implementar as partições dinâmicas

Como: Implementar um Partitioner com um número de estático de partições

Conceitos

Programação em paralela a.NET Framework