Sdílet prostřednictvím


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 je Action<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říklad ImageSharp.
  • 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 u struct zpětných volání předávaných ParallelHelper rozhraním API: jsou IAction, IAction2DIRefAction<T> a IInAction<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.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 zachytáváme cílové pole přímo a používáme IActioninterface 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í IActionrozhraním , IRefAction<T>IAction2Da 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 IActionIAction2D 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

Další příklady najdete v testech jednotek.