Partilhar via


ParallelHelper

O ParallelHelper contém APIs de alto desempenho para trabalhar com código paralelo. Ele contém métodos orientados ao desempenho que podem ser usados para configurar e executar rapidamente operações paralelas em um determinado conjunto de dados ou intervalo ou área de iteração.

APIs de plataforma: ParallelHelper, IAction, IAction2D, IRefAction<T>, IInAction<T><T>

Como ele funciona

ParallelHelper tipo é construído em torno de três conceitos principais:

  • Ele executa lotes automáticos no intervalo de iteração de destino. Isso significa que ele agenda automaticamente o número certo de unidades de trabalho com base no número de núcleos de CPU disponíveis. Isso é feito para reduzir a sobrecarga de invocar o retorno de chamada paralelo uma vez para cada iteração paralela.
  • Ele aproveita fortemente a maneira como os tipos genéricos são implementados em C# e usa struct tipos que implementam interfaces específicas em vez de delegados como Action<T>. Isso é feito para que o compilador JIT seja capaz de "ver" cada tipo de retorno de chamada individual que está sendo usado, o que permite incorporar totalmente o retorno de chamada, quando possível. Isso pode reduzir bastante a sobrecarga de cada iteração paralela, especialmente ao usar retornos de chamada muito pequenos, o que teria um custo trivial apenas com relação à invocação do delegado. Além disso, usar um tipo struct como retorno de chamada exige que os desenvolvedores manipulem manualmente as variáveis que estão sendo capturadas no fechamento, o que evita capturas acidentais do ponteiro this de métodos de instância e outros valores que poderiam desacelerar consideravelmente cada invocação de retorno de chamada. Essa é a mesma abordagem usada em outras bibliotecas orientadas para desempenho, como ImageSharp.
  • Ele expõe 4 tipos de APIs que representam 4 tipos diferentes de iterações: loops 1D e 2D, iteração de itens com efeito colateral e iteração de itens sem efeito colateral. Cada tipo de ação tem um tipo interface correspondente que precisa ser aplicado aos struct retornos de chamada que estão sendo passados para as ParallelHelper APIs: esses são IAction, IAction2D, IRefAction<T> e IInAction<T><T>. Isso ajuda os desenvolvedores a escrever códigos mais claros quanto à sua intenção e permite que as APIs realizem otimizações adicionais internamente.

Sintaxe

Digamos que estamos interessados em processar todos os itens em algum array float[] e multiplicar cada um deles por 2. Nesse caso não precisamos capturar nenhuma variável: podemos apenas usar IRefAction<T> interface e ParallelHelper para carregar cada item para alimentar nosso retorno de chamada automaticamente. Basta definir nosso callback, que receberá um ref float argumento e realizará a operação necessária:

// Be sure to include this using at the top of the file:
using Microsoft.Toolkit.HighPerformance.Helpers;

// First declare the struct callback
public readonly struct ByTwoMultiplier : IRefAction<float>
{
    public void Invoke(ref float x) => x *= 2;
}

// Create an array and run the callback
float[] array = new float[10000];

ParallelHelper.ForEach<float, ByTwoMultiplier>(array);

Com a API ForEach, não precisamos especificar os intervalos de iteração: ParallelHelper agrupará a coleção e processará cada item de entrada automaticamente. Além disso, neste exemplo específico nem tivemos que passar nosso struct como argumento: como ele não continha nenhum campo que precisávamos inicializar, poderíamos apenas especificar seu tipo como um argumento de tipo ao invocar ParallelHelper.ForEach: essa API irá em seguida, crie uma nova instância disso struct por conta própria e use-a para processar os vários itens.

Para introduzir o conceito de encerramentos, suponha que queremos multiplicar os elementos do array por um valor que é especificado em tempo de execução. Para fazer isso, precisamos “capturar” esse valor em nosso tipo de retorno de chamada struct. Podemos fazer isso assim:

public readonly struct ItemsMultiplier : IRefAction<float>
{
    private readonly float factor;
    
    public ItemsMultiplier(float factor)
    {
        this.factor = factor;
    }

    public void Invoke(ref float x) => x *= this.factor;
}

// ...

ParallelHelper.ForEach(array, new ItemsMultiplier(3.14f));

Podemos ver que struct now contém um campo que representa o fator que queremos usar para multiplicar os elementos, em vez de usar uma constante. E ao invocar ForEach, estamos criando explicitamente uma instância do nosso tipo de retorno de chamada, com o fator que nos interessa. Além disso, neste caso, o compilador C# também é capaz de reconhecer automaticamente os argumentos de tipo que estamos usando, para que possamos omiti-los juntos na invocação do método.

Essa abordagem de criação de campos para valores que precisamos acessar a partir de um retorno de chamada nos permite declarar explicitamente quais valores queremos capturar, o que ajuda a tornar o código mais expressivo. Isso é exatamente a mesma coisa que o compilador C# faz nos bastidores quando declaramos uma função lambda ou função local que também acessa alguma variável local.

Aqui está outro exemplo, dessa vez usando a API For para inicializar todos os itens de um array em paralelo. Observe como dessa vez estamos capturando a matriz de destino diretamente e usando o IAction interface para nosso retorno de chamada, que fornece ao nosso método o índice de iteração paralela atual como argumento:

public readonly struct ArrayInitializer : IAction
{
    private readonly int[] array;

    public ArrayInitializer(int[] array)
    {
        this.array = array;
    }

    public void Invoke(int i)
    {
        this.array[i] = i;
    }
}

// ...

ParallelHelper.For(0, array.Length, new ArrayInitializer(array));

Observação

Como os tipos de retorno de chamada são struct-s, eles são passados por cópia para cada thread em execução paralela, não por referência. Isso significa que os tipos de valor armazenados como campos em tipos de retorno de chamada também serão copiados. Uma boa prática para lembrar esse detalhe e evitar erros é marcar o callback struct como readonly, para que o compilador C# não nos deixe modificar os valores de seus campos. Isso se aplica apenas a campos de instância de um tipo de valor: se um retorno de chamada struct tiver um campo static de qualquer tipo ou um campo de referência, então esse valor será compartilhado corretamente entre threads paralelos.

Métodos

Essas são as 4 APIs principais expostas por ParallelHelper, correspondentes às interfaces IAction, IAction2D, IRefAction<T> e IInAction<T>. O tipo ParallelHelper também expõe uma série de sobrecargas para esses métodos, que oferecem diversas maneiras de especificar o(s) intervalo(s) de iteração ou o tipo de retorno de chamada de entrada. For e For2D funcionam em instâncias IAction e IAction2D, e devem ser usados quando algum trabalho paralelo precisa ser feito que não precisa ser mapeado para uma coleção subjacente que pode ser acessada diretamente com os índices de cada iteração paralela. As sobrecargas ForEach funcionam em instâncias IRefAction<T> e IInAction<T> podem ser usadas quando as iterações paralelas mapeiam diretamente para itens em uma coleção que pode ser indexada diretamente. Nesse caso, eles também abstraem a lógica de indexação, de modo que cada invocação paralela só precisa se preocupar com o item de entrada no qual trabalhar, e não com a forma de recuperar esse item também.

Exemplos

Você pode encontrar mais exemplos nos testes de unidade.