Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
A ParallelHelper classe contém APIs de alto desempenho para trabalhar com código paralelo. Contém métodos orientados para desempenho que podem ser usados para configurar e executar rapidamente operações paralelas sobre um dado conjunto de dados, intervalo de iteração ou área.
APIs da plataforma:
ParallelHelper,IAction,IAction2D,IRefAction<T>,IInAction<T>
Como funciona
O ParallelHelper tipo baseia-se em três conceitos principais:
- Executa o processamento em lote automático no intervalo de iteração alvo. Esta funcionalidade agenda automaticamente o número correto de unidades de trabalho com base no número de núcleos de CPU disponíveis. Esta funcionalidade reduz a sobrecarga de invocar o callback paralelo uma vez por cada iteração paralela.
- Aproveita fortemente a forma como os tipos genéricos são implementados em C#. Utiliza tipos de
structque implementam interfaces específicas em vez de delegados comoAction<T>. Ao usar esta abordagem, o compilador JIT pode "ver" cada tipo individual de callback utilizado, o que significa que pode fazer o callback em linha inteiramente quando possível. Esta abordagem reduz muito a sobrecarga de cada iteração paralela, especialmente quando se usam callbacks muito pequenos. A sobrecarga da invocação de delegados seria trivial de outra forma. Além disso, ao usar umstructtipo como callback, é necessário o tratamento manual das variáveis capturadas na clausura. Este requisito previne capturas acidentais dothisponteiro a partir de métodos de instância e outros valores que poderiam atrasar consideravelmente cada invocação de callback. Esta abordagem é a mesma usada noutras bibliotecas orientadas para desempenho, comoImageSharp. - Expõe quatro tipos de APIs que representam quatro tipos diferentes de iterações: ciclos 1D e 2D, iteração de itens com efeito secundário e iteração de itens sem efeito secundário. Cada tipo de ação tem um tipo correspondente
interfaceque precisa de aplicar aosstructcallbacks que passa para asParallelHelperAPIs. Estas interfaces sãoIAction,IAction2D,IRefAction<T>, eIInAction<T>. Ao usar estas interfaces, pode escrever código que expressa claramente a sua intenção, e as APIs podem realizar otimizações internas.
Sintaxe
Digamos que estamos interessados em processar todos os itens em alguma float[] matriz e multiplicar cada um deles por 2. Neste caso, não precisamos capturar nenhuma variável: podemos apenas usar o IRefAction<T>interface e ParallelHelper carregaremos cada item para alimentar nosso retorno de chamada automaticamente. Tudo o que é necessário é definir o nosso callback, que receberá um argumento ref float e executará 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 ForEach API, não precisamos especificar os intervalos de iteração: ParallelHelper agruparemos a coleção em lote e processaremos cada item de entrada automaticamente. Além disso, neste exemplo específico, nem precisávamos passar o 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 criará uma nova instância disso struct por conta própria e usará isso para processar os vários itens.
Para introduzir o conceito de fechamentos, suponha que queremos multiplicar os elementos da matriz por um valor especificado em tempo de execução. Para tal, precisamos "capturar" esse valor no nosso tipo de callback struct. Podemos fazê-lo 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 o struct agora contém um campo que representa o fator que queremos usar para multiplicar elementos, em vez de usar uma constante. E quando invocamos ForEach, estamos explicitamente a criar uma instância do nosso tipo de função de retorno, 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 da chamada do método.
Essa abordagem de criar 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 acessa alguma variável local também.
Aqui está outro exemplo, desta vez usando a For API para inicializar todos os itens de uma matriz em paralelo. Observe como, desta vez, estamos a capturar a matriz de destino diretamente e estamos a usar o IActioninterface para o nosso retorno de chamada, que dá 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 que estão sendo armazenados como campos em um tipo de retorno de chamada também serão copiados. Uma boa prática para lembrar esse detalhe e evitar erros é marcar o retorno de chamada struct como readonly, para que o compilador C# não nos deixe modificar os valores de seus campos. Isso só se aplica a campos de instância de um tipo de valor: se um retorno de chamada struct tiver um static campo de qualquer tipo ou um campo de referência, esse valor será compartilhado corretamente entre threads paralelos.
Metodologia
Estas são as 4 principais APIs expostas por ParallelHelper, correspondendo às interfaces IAction, IAction2D, IRefAction<T> e IInAction<T>. O ParallelHelper tipo também expõe uma série de sobrecargas para esses métodos, que oferecem várias maneiras de especificar o(s) intervalo(s) de iteração ou o tipo de retorno de chamada de entrada.
For e For2D atuam em IAction e IAction2D instâncias, e são destinados a serem usados quando é necessário realizar trabalho paralelo que não precisa mapear para uma coleção subjacente acessível diretamente pelos índices de cada iteração paralela. Em ForEach vez disso, as sobrecargas wotk on IRefAction<T> e IInAction<T> instances, e elas podem ser usadas quando as iterações paralelas são mapeadas diretamente para itens em uma coleção que pode ser indexada diretamente. Neste caso, eles também abstraem a lógica de indexação, de forma que cada invocação paralela somente precisa se preocupar com o item de entrada para trabalhar, e não com a forma de recuperar esse item também.
Exemplos
Pode encontrar mais exemplos nos testes unitários
.NET Community Toolkit