Třída ParallelHelper

Třída ParallelHelper obsahuje 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, IActionIAction2D, , IRefAction<T>,IInAction<T>

Jak to funguje

Typ ParallelHelper je postaven na třech hlavních konceptech:

  • Provádí automatické dávkování v cílovém rozsahu iterací. Tato funkce automaticky naplánuje správný počet pracovních jednotek na základě počtu dostupných jader procesoru. Tato funkce snižuje režii spojenou s vyvoláním paralelního zpětného volání, které probíhá jednou při každé paralelní iteraci.
  • Výrazně využívá způsob implementace obecných typů v jazyce C#. Používá struct typy, které implementují specifická rozhraní místo delegátů, jako je Action<T>. Pomocí tohoto přístupu může kompilátor JIT identifikovat každý jednotlivý typ zpětného volání, který je používán, což znamená, že může zcela integrovat zpětné volání, pokud je to možné. Tento přístup výrazně snižuje režijní náklady na každou paralelní iteraci, zejména při použití velmi malých zpětných volání. Náklad na vyvolání delegáta by jinak byl triviální. Pokud navíc použijete typ struct jako zpětné volání, musíte ručně zpracovat proměnné zachycené v uzávěru. Tento požadavek brání náhodnému zachycení ukazatele this metodami instance a jinými hodnotami, které by mohly výrazně zpomalit jednotlivé vyvolání. Tento přístup je stejný jako v jiných knihovnách orientovaných na výkon, například ImageSharp.
  • Zpřístupňuje čtyři typy rozhraní API, které představují čtyři 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ý musíte použít pro struct zpětná volání, která předáte ParallelHelper rozhraním API. Tato rozhraní jsou IAction, IAction2D, IRefAction<T>a IInAction<T>. Pomocí těchto rozhraní můžete napsat kód, který jasně vyjadřuje svůj záměr, a rozhraní API můžou interně provádět další optimalizace.

Syntaxe

Řekněme, že nás zajímá zpracování všech položek v nějakém float[] poli a vynásobení každé z nich 2. V tomto případě nemusíme zaznamenávat žádné proměnné: můžeme jednoduše použít IRefAction<T>interface, a ParallelHelper automaticky načte každou položku pro naše zpětné volání. Stačí definovat naše zpětné volání, které obdrží argument ref float 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.ForEachtohoto 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í ForEachexplicitně 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 přímo zachytáváme cílové pole a používáme IActioninterface pro naše zpětné volání, což dává naší metodě aktuální index paralelní iterace 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 typu struct, předávají se kopírováním do každého paralelně běžícího vlákna, nikoli odkazem. To znamená, že hodnoty, které se ukládají jako pole v typech zpětného volání, se zkopírují také. Dobrou praxí, jak si zapamatovat detaily a vyhnout se chybám, je označit zpětné volání struct jako readonly, aby nám kompilátor jazyka C# nedovolil upravovat hodnoty jeho 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á `ParallelHelper`, která odpovídají rozhraním `IAction`, `IAction2D`, `IRefAction<T>` a `IInAction<T>`. 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 pracují na IAction a IAction2D instancích a jsou určeny k použití, když je potřeba provádět některé paralelní úkoly, které není nutné mapovat 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 pracují na IRefAction<T> a IInAction<T> instancech a lze je použít, když paralelní iterace přímo odpovídají položkám v kolekci, které lze indexovat přímo. V tomto případě také abstrahují logiku indexování, aby se každé paralelní vyvolání muselo starat pouze o vstupní položku, kterou má zpracovávat, a ne o to, jak tuto položku načíst.

Příklady

Další příklady najdete v jednotkových testech.