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
, ,IAction
IAction2D
, ,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 jakAction<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życiestruct
typu jako wywołania zwrotnego wymaga od deweloperów ręcznego obsługi zmiennych przechwytywanych w zamknięciu, co zapobiega przypadkowemu przechwytywaniuthis
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 jakImageSharp
. - 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 doParallelHelper
struct
interfejsów API: sąIAction
to ,IAction2D
IRefAction<T>
iIInAction<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 2
element . 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 ForEach
metody 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 ParallelHelper
interfejsy , odpowiadające interfejsom IRefAction<T>
IAction
IAction2D
, 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. For
i 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
.NET Community Toolkit