Udostępnij za pośrednictwem


ParallelHelper

Zawiera ParallelHelper interfejsy API o wysokiej wydajności do pracy z kodem równoległym. Zawiera metody zorientowane na wydajność, które mogą służyć do szybkiego konfigurowania i wykonywania operacji równoległych w danym zestawie danych lub zakresie iteracji lub obszarze.

Interfejsy API platformy: ParallelHelper, , IActionIAction2D, , IRefAction<T>IInAction<T><T>

Jak to działa

ParallelHelper Typ jest tworzony wokół trzech głównych pojęć:

  • Wykonuje automatyczne przetwarzanie wsadowe w zakresie iteracji docelowej. Oznacza to, że automatycznie planuje odpowiednią liczbę jednostek roboczych na podstawie liczby dostępnych rdzeni procesora CPU. Należy to zrobić, aby zmniejszyć obciążenie wywołania zwrotnego równoległego raz dla każdej iteracji równoległej.
  • W dużym stopniu wykorzystuje sposób implementowania typów ogólnych w języku C#i używa struct typów implementowania określonych interfejsów zamiast delegatów, takich jak Action<T>. Dzieje się tak, aby kompilator JIT mógł "zobaczyć" używany pojedynczy typ wywołania zwrotnego, co pozwala na całkowite wyliniowanie wywołania zwrotnego, gdy jest to możliwe. Może to znacznie zmniejszyć obciążenie każdej iteracji równoległej, zwłaszcza w przypadku korzystania z bardzo małych wywołań zwrotnych, które byłyby kosztem trywialnym w odniesieniu do samego wywołania delegata. Ponadto użycie struct typu jako wywołania zwrotnego wymaga od deweloperów ręcznego obsługi zmiennych przechwytywanych w zamknięciu, co zapobiega przypadkowemu przechwytywaniu this wskaźnika z metod wystąpienia i innych wartości, które mogą znacznie spowolnić każde wywołanie wywołania zwrotnego. Jest to takie samo podejście, które jest używane w innych bibliotekach zorientowanych na wydajność, takich jak ImageSharp.
  • Uwidacznia 4 typy interfejsów API reprezentujących 4 różne typy iteracji: pętle 1D i 2D, iterację elementów z efektem ubocznym i iteracją elementów bez efektu bocznego. Każdy typ akcji ma odpowiedni interface typ, który należy zastosować do wywołań zwrotnych przekazywanych do ParallelHelper struct interfejsów API: są IActionto , IAction2DIRefAction<T> i IInAction<T><T>. Ułatwia to deweloperom pisanie kodu, który jest jaśniejszy w odniesieniu do jego intencji, i umożliwia interfejsom API wykonywanie dalszych optymalizacji wewnętrznie.

Składnia

Załóżmy, że interesuje nas przetwarzanie wszystkich elementów w tablicy float[] i pomnożenie każdego z nich przez 2element . W takim przypadku nie musimy przechwytywać żadnych zmiennych: możemy po prostu użyć IRefAction<T> interface elementu i ParallelHelper załaduje każdy element do automatycznego wywołania zwrotnego. Wystarczy zdefiniować nasze wywołanie zwrotne, które otrzyma argument i wykona niezbędną operację ref float :

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

Za pomocą interfejsu ForEach API nie musimy określać zakresów iteracji: ParallelHelper spowoduje wsadowanie kolekcji i przetworzenie każdego elementu wejściowego automatycznie. Ponadto w tym konkretnym przykładzie nie musieliśmy nawet przekazać argumentu struct jako argumentu: ponieważ nie zawiera żadnych pól, które musieliśmy zainicjować, możemy po prostu określić jego typ jako argument typu podczas wywoływania ParallelHelper.ForEach: ten interfejs API utworzy nowe wystąpienie tego struct obiektu samodzielnie i użyjemy go do przetworzenia różnych elementów.

Aby wprowadzić koncepcję zamknięcia, załóżmy, że chcemy pomnożyć elementy tablicy przez wartość określoną w czasie wykonywania. W tym celu musimy "przechwycić" wartość w naszym typie wywołania zwrotnego struct . Możemy to zrobić w następujący sposób:

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

Widzimy, że teraz struct zawiera pole reprezentujące czynnik, którego chcemy użyć do mnożenia elementów, zamiast używać stałej. Podczas wywoływania ForEachmetody jawnie tworzymy wystąpienie typu wywołania zwrotnego z czynnikiem, który nas interesuje. Ponadto w tym przypadku kompilator języka C# może automatycznie rozpoznawać używane argumenty typu, dzięki czemu możemy pominąć je razem z wywołania metody.

Takie podejście do tworzenia pól dla wartości, do których musimy uzyskać dostęp z wywołania zwrotnego, umożliwia jawne zadeklarowanie wartości, które chcemy przechwycić, co pomaga w bardziej wyrazistym kodzie. Jest to dokładnie to samo, co kompilator języka C# wykonuje za kulisami, gdy deklarujemy funkcję lambda lub funkcję lokalną, która uzyskuje dostęp do jakiejś zmiennej lokalnej.

Oto kolejny przykład, tym razem przy użyciu interfejsu For API do zainicjowania wszystkich elementów tablicy równolegle. Zwróć uwagę, jak tym razem przechwytujemy tablicę docelową bezpośrednio i używamy IAction interface elementu dla wywołania zwrotnego, co daje naszej metodzie bieżący indeks iteracji równoległej 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));

Uwaga

Ponieważ typy wywołań zwrotnych to struct-s, są przekazywane przez kopię do każdego wątku uruchomionego równolegle, a nie przez odwołanie. Oznacza to, że typy wartości przechowywane jako pola w typach wywołań zwrotnych również zostaną skopiowane. Dobrym rozwiązaniem, aby pamiętać, że szczegóły i uniknąć błędów jest oznaczenie wywołania zwrotnego struct jako readonly, aby kompilator języka C# nie pozwolił nam modyfikować wartości jego pól. Dotyczy to tylko pól wystąpienia typu wartości: jeśli wywołanie zwrotne struct ma static pole dowolnego typu lub pole odwołania, ta wartość będzie poprawnie współdzielona między wątkami równoległymi.

Metody

Są to 4 główne interfejsy API udostępniane przez ParallelHelperinterfejsy , odpowiadające interfejsom IRefAction<T> IActionIAction2D, i .IInAction<T> Typ ParallelHelper uwidacznia również szereg przeciążeń dla tych metod, które oferują szereg sposobów określania zakresów iteracji lub typu wywołania zwrotnego danych wejściowych. Fori pracować nad IAction IAction2D wystąpieniami i For2D są one przeznaczone do użycia, gdy należy wykonać pewną równoległą pracę, która nie wymaga mapowania na podstawową kolekcję, do której można uzyskać bezpośredni dostęp z indeksami każdej iteracji równoległej. Przeciążenia ForEach zamiast wotk on IRefAction<T> i IInAction<T> wystąpienia i mogą być używane, gdy iteracje równoległe są bezpośrednio mapowane na elementy w kolekcji, które można indeksować bezpośrednio. W takim przypadku również abstrakują logikę indeksowania, aby każda równoległa wywołanie musi martwić się tylko o element wejściowy do pracy, a nie na sposób pobierania tego elementu.

Przykłady

Więcej przykładów można znaleźć w testach jednostkowych.