ParallelHelper
Obsahuje ParallelHelper
vysoce výkonná rozhraní API pro práci s paralelním kódem. Obsahuje metody orientované na výkon, které lze použít k rychlému nastavení a provádění paralelních operací v dané sadě dat nebo oblasti iterace.
Rozhraní API platformy:
ParallelHelper
,IAction
IAction2D
, ,IRefAction<T>
,IInAction<T><T>
Jak to funguje
ParallelHelper
typ je postaven na třech hlavních konceptech:
- Provádí automatické dávkování v cílovém rozsahu iterací. To znamená, že automaticky naplánuje správný počet pracovních jednotek na základě počtu dostupných jader procesoru. Tím se sníží režijní náklady na vyvolání paralelního zpětného volání jednou pro každou paralelní iteraci.
- Výrazně využívá způsob implementace obecných typů v jazyce C# a používá
struct
typy implementované konkrétní rozhraní místo delegátů, jako jeAction<T>
. To se provádí tak, aby kompilátor JIT mohl "zobrazit" každý jednotlivý typ zpětného volání, který umožňuje vložit zpětné volání zcela, pokud je to možné. To může výrazně snížit režii každé paralelní iterace, zejména při použití velmi malých zpětných volání, které by měly triviální náklady s ohledem na samotné vyvolání delegáta. Kromě toho použitístruct
typu jako zpětného volání vyžaduje, aby vývojáři ručně zpracovávali proměnné zachycené v uzavření, což brání náhodnému zachyceníthis
ukazatele z metod instance a dalších hodnot, které by mohly výrazně zpomalit každé vyvolání zpětného volání. Jedná se o stejný přístup, který se používá v jiných knihovnách orientovaných na výkon, napříkladImageSharp
. - Zpřístupňuje 4 typy rozhraní API, které představují 4 různé typy iterací: 1D a 2D smyčky, iterace položek s vedlejším účinkem a iterace položek bez vedlejšího efektu. Každý typ akce má odpovídající
interface
typ, který je potřeba použít ustruct
zpětných volání předávanýchParallelHelper
rozhraním API: jsouIAction
,IAction2D
IRefAction<T>
aIInAction<T><T>
. To vývojářům pomáhá psát kód, který je jasnější ohledně jeho záměru, a umožňuje rozhraním API interně provádět další optimalizace.
Syntaxe
Řekněme, že nás zajímá zpracování všech položek v určitém float[]
poli a jejich vynásobení .2
V tomto případě nemusíme zaznamenávat žádné proměnné: můžeme jednoduše použít IRefAction<T>
interface
a ParallelHelper
načteme každou položku, která se bude automaticky načítat do zpětného volání. Vše, co je potřeba k definování zpětného volání, které obdrží ref float
argument a provede potřebnou operaci:
// 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);
ForEach
U rozhraní API nemusíme zadávat rozsahy iterací: ParallelHelper
vysádí kolekci a zpracuje každou vstupní položku automaticky. V tomto konkrétním příkladu jsme navíc nemuseli předat ani náš struct
argument: protože neobsahuje žádná pole, která jsme potřebovali k inicializaci, mohli bychom při vyvolání ParallelHelper.ForEach
tohoto rozhraní API jednoduše zadat jeho typ jako argument typu: toto rozhraní API pak vytvoří novou instanci této struct
vlastní instance a použije ji ke zpracování různých položek.
Abychom představili koncept uzavření, předpokládejme, že chceme násobit prvky pole hodnotou zadanou za běhu. K tomu musíme "zachytit" danou hodnotu v našem typu zpětného struct
volání. Můžeme to udělat takto:
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));
Vidíme, že struct
teď obsahuje pole, které představuje faktor, který chceme použít k násobení prvků, místo použití konstanty. A při vyvolání ForEach
explicitně vytváříme instanci typu zpětného volání s faktorem, který nás zajímá. V tomto případě navíc kompilátor jazyka C# dokáže automaticky rozpoznat argumenty typu, které používáme, takže je můžeme vynechat společně z vyvolání metody.
Tento přístup k vytváření polí pro hodnoty, ke kterým potřebujeme získat přístup z zpětného volání, nám umožňuje explicitně deklarovat hodnoty, které chceme zachytit, což pomáhá kód vyjádřit. To je úplně totéž, co kompilátor jazyka C# provádí na pozadí, když deklarujeme funkci lambda nebo místní funkci, která přistupuje také k nějaké místní proměnné.
Tady je další příklad, kdy tentokrát pomocí For
rozhraní API inicializujete všechny položky pole paralelně. Všimněte si, jak tentokrát zachytáváme cílové pole přímo a používáme IAction
interface
pro zpětné volání metodu aktuální paralelní iterační index jako argument:
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));
Poznámka:
Vzhledem k tomu, že typy zpětného volání jsou struct
-s, předávají se kopírováním do každého vlákna spuštěného paralelně, nikoli odkazem. To znamená, že hodnoty, které se ukládají jako pole v typech zpětného volání, se zkopírují také. Osvědčeným postupem je zapamatovat si, že podrobnosti a vyhnout se chybám je označit zpětné struct
volání jako readonly
, aby kompilátor jazyka C# nám nedovolil upravit hodnoty polí. To platí jenom pro pole instance typu hodnoty: pokud zpětné struct
volání obsahuje static
pole libovolného typu nebo referenční pole, bude tato hodnota správně sdílena mezi paralelními vlákny.
Metody
Jedná se o 4 hlavní rozhraní API vystavená rozhraními ParallelHelper
, která odpovídají IAction
rozhraním , IRefAction<T>
IAction2D
a IInAction<T>
rozhraním. Typ ParallelHelper
také zveřejňuje řadu přetížení pro tyto metody, které nabízejí řadu způsobů, jak určit rozsahy iterací nebo typ zpětného volání vstupu. For
a For2D
pracovat na IAction
IAction2D
instancích a mají být použity, když je potřeba provést některé paralelní práce, které nevyžadují mapování na podkladovou kolekci, ke které je možné přistupovat přímo pomocí indexů každé paralelní iterace. Přetížení ForEach
místo toho wotk on IRefAction<T>
a IInAction<T>
instance, a lze je použít, když paralelní iterace přímo mapují na položky v kolekci, které lze indexovat přímo. V tomto případě také abstrahují logiku indexování, aby se každé paralelní vyvolání museli starat pouze o vstupní položku, na které se má pracovat, a ne na tom, jak tuto položku načíst.
Příklady
.NET Community Toolkit