Dela via


ParallelHelper

Innehåller ParallelHelper API:er med höga prestanda för att fungera med parallell kod. Den innehåller prestandaorienterade metoder som kan användas för att snabbt konfigurera och köra parallella åtgärder över en viss datauppsättning eller iterationsintervall eller område.

Plattforms-API:er:ParallelHelper, IAction, IAction2D, IRefAction<T>, IInAction<T>

Så här fungerar det

ParallelHelper typ bygger på tre huvudkoncept:

  • Den utför automatisk batchning över det målinriktade iterationsintervallet. Det innebär att det automatiskt schemalägger rätt antal arbetsenheter baserat på antalet tillgängliga CPU-kärnor. Detta görs för att minska kostnaderna för att anropa parallell återanrop en gång för varje parallell iteration.
  • Det utnyttjar kraftigt hur generiska typer implementeras i C# och använder struct typer som implementerar specifika gränssnitt i stället för ombud som Action<T>. Detta görs så att JIT-kompilatorn kan "se" varje enskild återanropstyp som används, vilket möjliggör att återanropet kan infogas helt när det är möjligt. Detta kan avsevärt minska kostnaderna för varje parallell iteration, särskilt när du använder mycket små återanrop, vilket skulle ha en trivial kostnad enbart för delegatanropet. Dessutom kräver användning av en struct-typ som återanrop att utvecklare manuellt hanterar variabler som fångas i slutningen, vilket förhindrar oavsiktliga fångster av this-pekaren från instansmetoder och andra värden som skulle kunna sakta ner varje återanrop betydligt. Det här är samma metod som används i andra prestandaorienterade bibliotek, ImageSharptill exempel .
  • Den exponerar 4 typer av API:er som representerar 4 olika typer av iterationer: 1D- och 2D-loopar, objekt-iteration med bieffekt och objekt-iteration utan bieffekt. Varje typ av åtgärd har en motsvarande interface typ som måste tillämpas på återkallningarna struct som skickas till API:erna ParallelHelper: dessa är IAction, IAction2D, IRefAction<T> och IInAction<T><T>. Detta hjälper utvecklare att skriva kod som är tydligare när det gäller dess avsikt och gör det möjligt för API:erna att utföra ytterligare optimeringar internt.

Syntax

Anta att vi är intresserade av att bearbeta alla objekt i en matris float[] och att multiplicera var och en av dem 2med . I det här fallet behöver vi inte samla in några variabler: vi kan bara använda IRefAction<T>interface och ParallelHelper läser in varje objekt för att mata in till vårt återanrop automatiskt. Allt som behövs är att definiera återanropet, som tar emot ett ref float argument och utför den nödvändiga åtgärden:

// 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);

Med API:et ForEach behöver vi inte ange iterationsintervallen: ParallelHelper batchar samlingen och bearbetar varje indataobjekt automatiskt. Dessutom behövde vi i det här specifika exemplet inte ens skicka vårt struct argument: eftersom det inte innehöll några fält som vi behövde initiera, kunde vi bara ange dess typ som ett typargument när vi anropade ParallelHelper.ForEach: det API:et skapar sedan en ny instans av det struct på egen hand och använder den för att bearbeta de olika objekten.

För att introducera konceptet closures, föreställer vi oss att vi vill multiplicera arrayelementen med ett värde som anges vid körning. För att göra det måste vi "fånga" det värdet i vår återanropstyp struct. Vi kan göra så här:

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));

Vi kan se att struct nu innehåller ett fält som representerar den faktor som vi vill använda för att multiplicera element, i stället för att använda en konstant. Och när vi anropar ForEach, skapar vi uttryckligen en instans av vår callback-typ, med den faktorn som vi är intresserade av. I det här fallet kan C#-kompilatorn dessutom automatiskt identifiera de typargument som vi använder, så att vi kan utelämna dem tillsammans från metodanropet.

Med den här metoden för att skapa fält för värden som vi behöver komma åt från ett återanrop kan vi uttryckligen deklarera vilka värden vi vill samla in, vilket gör koden mer uttrycksfull. Det här är exakt samma sak som C#-kompilatorn gör i bakgrunden när vi deklarerar en lambda-funktion eller lokal funktion som också har åtkomst till någon lokal variabel.

Här är ett annat exempel, den här gången använder API:et For för att initiera alla objekt i en matris parallellt. Observera hur vi den här gången samlar in målmatrisen direkt, och vi använder för återanropet IActioninterface , vilket ger vår metod det aktuella parallella iterationsindexet som 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));

Anmärkning

Eftersom återuppringningstyperna är struct-s, överförs de genom kopiering till varje tråd som körs parallellt, inte som referens. Det innebär att även värdetyper som lagras som fält i återkallningstyper kommer att kopieras. En bra idé att komma ihåg den informationen och undvika fel är att markera återanropet struct som readonly, så att C#-kompilatorn inte låter oss ändra värdena för dess fält. Detta gäller endast instansfält av en värdetyp: om ett återanrop struct har ett static fält av någon typ eller ett referensfält delas det värdet korrekt mellan parallella trådar.

Metoder

Det här är de 4 huvudsakliga API:erna som exponeras av ParallelHelper, som motsvarar IAction, IAction2DIRefAction<T> och IInAction<T> -gränssnitten. Typen ParallelHelper exponerar också ett antal överlagringar för dessa metoder, som erbjuder ett antal sätt att ange iterationsintervall eller typ av motringning av indata. For och For2D arbeta med IAction och IAction2D instanser, och de är avsedda att användas när en del parallellt arbete behöver utföras som inte behöver mappas till en underliggande samling som kan nås direkt med indexen för varje parallell iteration. Överlagringarna ForEach wotk i stället på IRefAction<T> och IInAction<T> instanser, och de kan användas när parallella iterationer mappas direkt till objekt i en samling som kan indexeras direkt. I det här fallet abstraherar de också bort indexeringslogiken, så att varje parallellt anrop bara behöver koncentrera sig på indataobjektet att arbeta med, och inte på hur de hämtar det objektet.

Exempel

Du kan hitta fler exempel i enhetstesterna .