ParallelHelper
ParallelHelper
Contiene API ad alte prestazioni da usare con codice parallelo. Contiene metodi orientati alle prestazioni che possono essere usati per configurare ed eseguire rapidamente operazioni parallele su un determinato set di dati o intervallo o area di iterazione.
API della piattaforma:
ParallelHelper
,IAction
,IAction2D
,IRefAction<T>
,IInAction<T><T>
Funzionamento
ParallelHelper
il tipo è basato su tre concetti principali:
- Esegue l'invio in batch automatico sull'intervallo di iterazione di destinazione. Ciò significa che pianifica automaticamente il numero corretto di unità di lavoro in base al numero di core CPU disponibili. Questa operazione viene eseguita per ridurre il sovraccarico di richiamo del callback parallelo una volta per ogni singola iterazione parallela.
- Sfrutta notevolmente il modo in cui i tipi generici vengono implementati in C# e usano tipi che implementano
struct
interfacce specifiche anziché delegati comeAction<T>
. Questa operazione viene eseguita in modo che il compilatore JIT sia in grado di "vedere" ogni singolo tipo di callback in uso, che consente di inline completamente il callback, quando possibile. Ciò può ridurre notevolmente il sovraccarico di ogni iterazione parallela, soprattutto quando si usano callback molto piccoli, che avrebbero un costo banale rispetto alla sola chiamata del delegato. Inoltre, l'uso di unstruct
tipo come callback richiede agli sviluppatori di gestire manualmente le variabili acquisite nella chiusura, che impedisce acquisizioni accidentali del puntatore dai metodi dithis
istanza e altri valori che potrebbero rallentare notevolmente ogni chiamata di callback. Si tratta dello stesso approccio usato in altre librerie orientate alle prestazioni,ImageSharp
ad esempio . - Espone 4 tipi di API che rappresentano 4 tipi diversi di iterazioni: cicli 1D e 2D, iterazione degli elementi con effetto collaterale e iterazione degli elementi senza effetti collaterali. Ogni tipo di azione ha un tipo corrispondente
interface
che deve essere applicato aistruct
callback passati alleParallelHelper
API: sonoIAction
,IAction2D
IRefAction<T>
eIInAction<T><T>
. Ciò consente agli sviluppatori di scrivere codice più chiaro per quanto riguarda la finalità e consente alle API di eseguire altre ottimizzazioni internamente.
Sintassi
Si supponga di essere interessati a elaborare tutti gli elementi in una float[]
matrice e moltiplicarli per 2
. In questo caso non è necessario acquisire alcuna variabile: è sufficiente usare IRefAction<T>
interface
e ParallelHelper
caricherà ogni elemento per il feed al callback automaticamente. Tutto ciò che serve è definire il callback, che riceverà un ref float
argomento ed eseguirà l'operazione necessaria:
// 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);
Con l'API ForEach
non è necessario specificare gli intervalli di iterazione: ParallelHelper
in batch la raccolta e l'elaborazione automatica di ogni elemento di input. Inoltre, in questo esempio specifico non è stato necessario passare struct
come argomento: poiché non contiene alcun campo da inizializzare, è possibile specificarne semplicemente il tipo come argomento di tipo quando si richiama ParallelHelper.ForEach
: tale API creerà quindi una nuova istanza di che struct
da sola e la userà per elaborare i vari elementi.
Per introdurre il concetto di chiusura, si supponga di voler moltiplicare gli elementi della matrice per un valore specificato in fase di esecuzione. A tale scopo, è necessario "acquisire" tale valore nel tipo di callback struct
. È possibile eseguire questa operazione in questo modo:
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));
È possibile notare che ora struct
contiene un campo che rappresenta il fattore da usare per moltiplicare gli elementi, anziché usare una costante. Quando si richiama ForEach
, si crea in modo esplicito un'istanza del tipo di callback, con il fattore a cui si è interessati. Inoltre, in questo caso il compilatore C# è anche in grado di riconoscere automaticamente gli argomenti di tipo in uso, in modo da poterli omettere insieme dalla chiamata al metodo.
Questo approccio alla creazione di campi per i valori a cui è necessario accedere da un callback consente di dichiarare in modo esplicito i valori da acquisire, che consente di rendere il codice più espressivo. Questa è esattamente la stessa cosa che il compilatore C# esegue in background quando si dichiara una funzione lambda o una funzione locale che accede anche ad alcune variabili locali.
Ecco un altro esempio, questa volta che si usa l'API For
per inizializzare tutti gli elementi di una matrice in parallelo. Si noti come questa volta si acquisisce direttamente la matrice di destinazione e si usa per il callback, che fornisce al IAction
interface
metodo l'indice di iterazione parallela corrente come argomento:
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));
Nota
Poiché i tipi di callback sono struct
-s, vengono passati dalla copia a ogni thread che esegue parallela, non per riferimento. Ciò significa che anche i tipi valore archiviati come campi in un tipo di callback verranno copiati. È consigliabile ricordare che i dettagli ed evitare errori consiste nel contrassegnare il callback struct
come readonly
, in modo che il compilatore C# non consenta di modificare i valori dei relativi campi. Questo vale solo per i campi dell'istanza di un tipo valore: se un callback struct
ha un static
campo di qualsiasi tipo o un campo di riferimento, tale valore verrà condiviso correttamente tra thread paralleli.
Metodi
Queste sono le 4 API principali esposte da ParallelHelper
, corrispondenti alle IAction
interfacce , IAction2D
IRefAction<T>
e IInAction<T>
. Il ParallelHelper
tipo espone anche un certo numero di overload per questi metodi, che offrono diversi modi per specificare gli intervalli di iterazione o il tipo di callback di input. For
e For2D
lavorare su IAction
istanze e IAction2D
e sono concepiti per essere usati quando è necessario eseguire alcune operazioni parallele che non sono necessarie per eseguire il mapping a una raccolta sottostante accessibile direttamente con gli indici di ogni iterazione parallela. Gli ForEach
overload vengono invece attivati su IRefAction<T>
istanze e IInAction<T>
e possono essere usati quando le iterazioni parallele eseguono direttamente il mapping agli elementi di una raccolta che possono essere indicizzati direttamente. In questo caso, estraggono anche la logica di indicizzazione, in modo che ogni chiamata parallela debba preoccuparsi solo dell'elemento di input per funzionare e non su come recuperare anche l'elemento.
Esempi
Altri esempi sono disponibili negli unit test.
.NET Community Toolkit