Freigeben über


ParallelHelper

Die ParallelHelper enthält Hochleistungs-APIs für die Arbeit mit parallelen Code. Sie enthält leistungsorientierte Methoden, die verwendet werden können, um parallele Vorgänge über ein bestimmtes Dataset oder einen Iterationsbereich schnell einzurichten und auszuführen.

Plattform-APIs: ParallelHelper, IAction, IAction2D, IRefAction<T>, IInAction<T><T>

Funktionsweise

Der ParallelHelper-Typ basiert auf drei Hauptkonzepten:

  • Er führt eine automatische Batchverarbeitung über den Zieliterationsbereich durch. Dies bedeutet, dass er die richtige Anzahl von Arbeitseinheiten basierend auf der Anzahl der verfügbaren CPU-Kerne automatisch plant. Dies geschieht, um den Aufwand für den einmaligen Aufruf des parallelen Rückrufs für jede einzelne parallele Iteration zu reduzieren.
  • Er nutzt stark die Art und Weise, wie generische Typen in C# implementiert werden, und verwendet struct-Typen, die bestimmte Schnittstellen implementieren, anstelle von Delegaten wie Action<T>. Dies geschieht, damit der JIT-Compiler jeden einzelnen verwendeten Rückruftyp „sehen“ kann, sodass er den Rückruf nach Möglichkeit vollständig inline bearbeiten kann. Dies kann den Aufwand für jede parallele Iteration erheblich reduzieren, insbesondere bei Verwendung sehr kleiner Rückrufe, deren Kosten im Vergleich zum Delegatenaufruf allein trivial wären. Darüber hinaus erfordert die Verwendung eines struct-Typs als Rückruf, dass Entwickler die Variablen, die beim Schließen aufgezeichnet werden, manuell verarbeiten müssen, was ein versehentliches Aufzeichnen des this-Zeigers von Instanzmethoden und anderen Werten verhindert, die jeden Rückrufaufruf erheblich verlangsamen könnten. Dies ist der gleiche Ansatz, der in anderen leistungsorientierten Bibliotheken verwendet wird, z. B. ImageSharp.
  • Es macht 4 Arten von APIs verfügbar, die 4 verschiedene Typen von Iterationen darstellen: 1D- und 2D-Schleifen, Elementiteration mit Nebenwirkung und Elementiteration ohne Nebenwirkung. Jeder Aktionstyp verfügt über einen entsprechenden interface-Typ, der auf die struct-Rückrufe angewendet werden muss, die von den ParallelHelper-APIs übergeben werden: dies sind IAction, IAction2D, IRefAction<T> und IInAction<T><T>. Auf diese Weise können Entwickler Code schreiben, der in Bezug auf ihre Absicht klarer ist, und ermöglicht es den APIs, intern weitere Optimierungen durchzuführen.

Syntax

Angenommen, wir sind daran interessiert, alle Elemente in einem float[]-Array zu verarbeiten und jedes davon mit 2 zu multiplizieren. In diesem Fall müssen keine Variablen aufgezeichnet werden. Sie können nur IRefAction<T> interface verwenden, und ParallelHelper lädt jedes Element, um es automatisch an den Rückruf zu übergeben. Wir müssen nur unseren Rückruf definieren, der ein ref float-Argument erhalten und den erforderlichen Vorgang ausführen wird:

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

Mit der ForEach-API müssen wir die Iterationsbereiche nicht angeben: ParallelHelper wird eine Batchverarbeitung der Sammlung durchführen und jedes Eingabeelements automatisch verarbeiten. Darüber hinaus mussten wir in diesem spezifischen Beispiel nicht einmal unser struct-Argument übergeben: Da es keine Felder enthielt, die wir initialisieren mussten, konnten wir beim Aufruf von ParallelHelper.ForEach seinen Typ einfach als Typargument angeben: diese API wird dann selbständig eine neue Instanz dieser struct erstellen und verwendet diese, um die verschiedenen Elemente zu verarbeiten.

Um das Konzept der Schließungen einzuführen, nehmen wir an, dass wir die Arrayelemente mit einem Wert multiplizieren möchten, der zur Laufzeit angegeben wird. Dazu müssen wir diesen Wert in unserem Rückruftyp struct „aufzeichnen“. Das können wir so tun:

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

Wir können sehen, dass die struct jetzt ein Feld enthält, das den Faktor darstellt, den wir zum Multiplizieren von Elementen verwenden möchten, anstatt eine Konstante zu verwenden. Und beim Aufrufen von ForEach erstellen wir explizit eine Instanz unseres Rückruftyps mit dem Faktor, an dem wir interessiert sind. Außerdem kann der C#-Compiler in diesem Fall automatisch die von uns verwendeten Typargumente erkennen, sodass wir sie aus dem Aufruf der Methode weglassen können.

Mit diesem Ansatz zum Erstellen von Feldern für Werte, auf die wir über einen Rückruf zugreifen müssen, können wir explizit deklarieren, welche Werte wir aufzeichnen wollen, wodurch der Code ausdrucksstärker wird. Dies ist genau dasselbe, was der C#-Compiler hinter den Kulissen ausführt, wenn wir eine Lambda- oder eine lokale Funktion deklarieren, die auch auf eine lokale Variable zugreift.

Hier ist ein weiteres Beispiel, dieses Mal unter Verwendung der For-API, um alle Elemente eines Arrays parallel zu initialisieren. Beachten Sie, dass wir dieses Mal das Zielarray direkt aufzeichnen und IAction interface für den Rückruf verwenden, wodurch unsere Methode den aktuellen parallelen Iterationsindex als Argument erhält:

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

Hinweis

Da es sich bei den Rückruftypen um struct-en handelt, werden sie durch Kopieren an jeden parallel ausgeführten Thread übergeben, nicht durch Verweis. Dies bedeutet, dass Werttypen, die als Felder in einem Rückruftyp gespeichert werden, ebenfalls kopiert werden. Eine bewährte Methode, sich an dieses Detail zu erinnern und Fehler zu vermeiden, besteht darin, den Rückruf struct als readonly zu markieren, damit der C#-Compiler uns die Werte seiner Felder nicht ändern lässt. Dies gilt nur für Instanz-Felder eines Werttyps: Wenn ein Rückruf struct über ein static-Feld eines beliebigen Typs oder über ein Verweisfeld verfügt, dann wird dieser Wert ordnungsgemäß zwischen parallelen Threads freigegeben.

Methoden

Dies sind die 4 Haupt-APIs, die durch ParallelHelper verfügbar gemacht werden, entsprechend den IAction-, IAction2D-, IRefAction<T>- und IInAction<T>-Schnittstellen. Der ParallelHelper-Typ macht auch eine Reihe von Überladungen für diese Methoden verfügbar, die eine Reihe von Möglichkeiten zum Angeben der Iterationsbereiche oder des Typs des Eingaberückrufs bieten. For und For2D funktionieren für IAction- und IAction2D-Instanzen, und sie sollen verwendet werden, wenn parallele Arbeiten durchgeführt werden müssen, die nicht unbedingt mit einer zugrunde liegenden Sammlung übereinstimmen, auf die direkt mit den Indizes jeder parallelen Iteration zugegriffen werden kann. Die ForEach-Überladungen funktionieren stattdessen für IRefAction<T>- und IInAction<T>-Instanzen, die verwendet werden können, wenn die parallelen Iterationen direkt mit Elementen in einer Sammlung übereinstimmen, die direkt indiziert werden können. In diesem Fall abstrahieren sie auch die Indizierungslogik, sodass sich jeder parallele Aufruf nur darum kümmern muss, dass das Eingabeelement funktioniert, und nicht darüber, wie dieses Element abgerufen werden kann.

Beispiele

Weitere Beispiele finden Sie in den Komponententests.