Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
A ParallelHelper classe 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 operações paralelas rapidamente em um determinado conjunto de dados ou intervalo de iteração ou área.
APIs de plataforma:
ParallelHelper,IAction,IAction2D,IRefAction<T>,IInAction<T>
Como ele funciona
O ParallelHelper tipo é criado em torno de três conceitos principais:
- Ele executa processamento em lotes automático no intervalo alvo de iteração. Esse recurso agenda automaticamente o número certo de unidades de trabalho com base no número de núcleos de CPU disponíveis. Esse recurso reduz 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 no C#. Ele utiliza tipos
structque implementam interfaces específicas em vez de delegar comoAction<T>. Usando essa abordagem, o compilador JIT pode "ver" cada tipo de callback individual sendo usado, o que significa que ele pode incorporar completamente o callback quando possível. Essa abordagem reduz muito a sobrecarga de cada iteração paralela, especialmente quando você usa retornos de chamada muito pequenos. A sobrecarga de invocação do delegado seria trivial. Além disso, ao usar um tipostructcomo callback, você precisa lidar manualmente com variáveis capturadas no fecho. Esse requisito impede capturas acidentais do ponteiro a partir de métodos de instância e outros valores que podem reduzir consideravelmente a velocidade de cada chamada de retorno. Essa abordagem é a mesma usada em outras bibliotecas orientadas a desempenho, comoImageSharp. - Ele expõe quatro tipos de APIs que representam quatro 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 correspondente
interfaceque deve ser aplicado aos callbacksstructque você passa para as APIsParallelHelper. Essas interfaces sãoIAction,IAction2DeIRefAction<T>IInAction<T>. Usando essas interfaces, você pode escrever um código que expresse claramente sua intenção e as APIs podem executar 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 no nosso tipo de callback 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 explicitamente criando uma instância do nosso tipo callback, 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 IActioninterface 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 callback são struct-s, eles são passados por cópia para cada thread em execução em paralelo, não por referência. Isso significa que 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 vez disso em instâncias IRefAction<T> e IInAction<T> e 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.
.NET Community Toolkit