ParallelHelper
Содержит ParallelHelper
высокопроизводительные API для работы с параллельным кодом. Он содержит ориентированные на производительность методы, которые можно использовать для быстрой настройки и выполнения параллельных операций по заданному набору данных или диапазону итерации или области.
API платформы:
ParallelHelper
,IAction
,IAction2D
IRefAction<T>
IInAction<T><T>
Принцип работы
ParallelHelper
Тип построен вокруг трех основных понятий:
- Он выполняет автоматическую пакетную обработку по целевому диапазону итерации. Это означает, что оно автоматически планирует правильное количество рабочих единиц на основе количества доступных ядер ЦП. Это делается для уменьшения затрат на вызов параллельного обратного вызова один раз для каждой параллельной итерации.
- Он в значительной степени использует способ реализации универсальных типов в C#, а также использует
struct
типы, реализующие определенные интерфейсы, а не делегатыAction<T>
. Это делается так, чтобы компилятор JIT мог видеть каждый отдельный тип обратного вызова, который позволяет полностью встраивать обратный вызов, когда это возможно. Это может значительно снизить затраты на каждую параллельную итерацию, особенно при использовании очень небольших обратных вызовов, что может иметь тривиальную стоимость в отношении вызова делегата только. Кроме того, использованиеstruct
типа в качестве обратногоthis
вызова требует от разработчиков вручную обрабатывать переменные, которые фиксируются в закрытии, что предотвращает случайное захват указателя из методов экземпляра и других значений, которые могут значительно замедлить каждое вызов обратного вызова. Это тот же подход, который используется в других библиотеках, ориентированных на производительность, напримерImageSharp
. - Он предоставляет 4 типа API, представляющих 4 различных типа итерации: циклы 1D и 2D, элементы итерации с побочным эффектом и элементы итерации без побочных эффектов. Каждый тип действия имеет соответствующий
interface
тип, который необходимо применить кstruct
обратным вызовам, передаваемымParallelHelper
в API: этоIAction
иIAction2D
.IRefAction<T>
IInAction<T><T>
Это помогает разработчикам писать код, более понятный в отношении его намерения, и позволяет API выполнять дальнейшие оптимизации внутри системы.
Синтаксис
Предположим, что мы заинтересованы в обработке всех элементов в определенном float[]
массиве и умножения каждого из них на 2
. В этом случае нам не нужно записывать переменные: мы можем просто использовать IRefAction<T>
interface
ParallelHelper
и загружать каждый элемент для автоматического канала обратного вызова. Все, что необходимо, заключается в определении обратного 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);
ForEach
При использовании API нам не нужно указывать диапазоны итерации: ParallelHelper
пакетирует коллекцию и обрабатывает каждый входной элемент автоматически. Кроме того, в этом конкретном примере мы даже не должны передавать наш struct
аргумент в качестве аргумента: так как он не содержал каких-либо полей, необходимых для инициализации, мы могли бы просто указать его тип в качестве аргумента типа при вызове ParallelHelper.ForEach
: этот API затем создаст новый экземпляр этого struct
по своему усмотрению, и использовать его для обработки различных элементов.
Чтобы ввести концепцию закрытия, предположим, что мы хотим умножить элементы массива на значение, указанное во время выполнения. Для этого необходимо "записать" это значение в нашем типе обратного struct
вызова. Мы можем сделать это так:
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));
Мы видим, что теперь struct
содержит поле, представляющее фактор, который мы хотим использовать для умножения элементов, а не с помощью константы. И при вызове ForEach
мы явно создадим экземпляр нашего типа обратного вызова с интересующим нас фактором. Кроме того, в этом случае компилятор C# также может автоматически распознавать аргументы типа, которые мы используем, поэтому мы можем опустить их вместе из вызова метода.
Этот подход к созданию полей для значений, к которым требуется получить доступ из обратного вызова, позволяет явно объявлять значения, которые мы хотим записать, что помогает сделать код более экспрессивным. Это точно то же самое, что компилятор C# делает за кулисами, когда мы объявляем лямбда-функцию или локальную функцию, которая также обращается к некоторой локальной переменной.
Ниже приведен еще один пример, на этот раз с помощью For
API для инициализации всех элементов массива параллельно. Обратите внимание, как на этот раз мы захватываем целевой массив напрямую, и мы используем IAction
interface
для обратного вызова наш метод, который предоставляет текущий индекс параллельной итерации в качестве аргумента:
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));
Примечание.
Так как типы обратного вызова являются struct
-s, они передаются по копированию в каждый поток, выполняющийся параллельно, а не по ссылке. Это означает, что типы значений, хранящиеся в виде полей в типах обратного вызова, также будут скопированы. Рекомендуется помнить, что подробные сведения и избежать ошибок заключается в том, чтобы пометить обратный struct
вызов как readonly
, чтобы компилятор C# не позволял нам изменять значения его полей. Это относится только к полям экземпляра типа значения: если обратный вызов имеет static
поле любого типа или ссылочного struct
поля, то это значение будет правильно использоваться между параллельными потоками.
Методы
Это 4 основных API, предоставляемых интерфейсамиParallelHelper
, соответствующими IRefAction<T>
IAction
IAction2D
интерфейсам и IInAction<T>
интерфейсам. Тип ParallelHelper
также предоставляет ряд перегрузок для этих методов, которые предлагают несколько способов указать диапазоны итерации или тип обратного вызова ввода. For
и For2D
работа над IAction
и IAction2D
экземплярами, и они предназначены для использования, когда необходимо выполнить параллельную работу, которая не требует сопоставления с базовой коллекцией, к которым можно напрямую обращаться с индексами каждой параллельной итерации. Перегрузки ForEach
, а не wotk в IRefAction<T>
и IInAction<T>
экземплярах, и их можно использовать, когда параллельные итерации напрямую сопоставляются с элементами в коллекции, которая может быть индексирована напрямую. В этом случае они также абстрагируют логику индексирования, чтобы каждый параллельный вызов должен беспокоиться только о входном элементе для работы, а не о том, как получить этот элемент.
Примеры
Дополнительные примеры можно найти в модульных тестах.
.NET Community Toolkit