Класс ParallelHelper

Класс ParallelHelper содержит API высокой производительности для работы с параллельным кодом. Он содержит ориентированные на производительность методы, которые можно использовать для быстрого настройки и выполнения параллельных операций по заданному набору данных или диапазону итерации или области.

API платформы:ParallelHelper, IAction, IAction2DIRefAction<T>IInAction<T>

Принцип работы

Тип ParallelHelper построен вокруг трех основных понятий:

  • Он выполняет автоматическую пакетную обработку по целевому диапазону итерации. Эта функция автоматически планирует правильное количество рабочих единиц на основе количества доступных ядер ЦП. Эта функция уменьшает издержки от вызова параллельного коллбэка каждый раз для каждой отдельной параллельной итерации.
  • Он в значительной степени использует способ реализации универсальных типов в C#. В нем используются struct типы, реализующие определенные интерфейсы, а не делегаты, например Action<T>. Используя этот подход, компилятор JIT может "видеть" каждый отдельный тип обратного вызова, который означает, что он может полностью встраить обратный вызов, когда это возможно. Этот подход значительно снижает затраты на каждую параллельную итерацию, особенно при использовании очень небольших обратных вызовов. Затраты на вызов делегата в противном случае будут тривиальными. Кроме того, используя тип struct в качестве обратного вызова, необходимо вручную обрабатывать переменные, захваченные в замыкании. Это требование предотвращает случайный захват this указателя из методов экземпляра и других значений, которые могут значительно замедлить вызов обратной функции. Такой же подход используется в других библиотеках, ориентированных на производительность, например ImageSharp.
  • Он предоставляет четыре типа API, которые представляют четыре разных типа итерации: 1D и 2D циклы, итерация элементов с побочными эффектами и итерация элементов без побочных эффектов. Каждый тип действия имеет соответствующий interface тип, который необходимо применить к struct обратным вызовам, передаваемым в ParallelHelper API. Эти интерфейсы: IAction, IAction2DIRefAction<T>и IInAction<T>. С помощью этих интерфейсов можно написать код, который четко выражает свое намерение, и API-интерфейсы могут выполнять дополнительные оптимизации внутри системы.

Синтаксис

Предположим, что мы заинтересованы в обработке всех элементов в определенном float[] массиве и умножения каждого из них на 2. В этом случае нам не нужно захватывать какие-либо переменные: мы можем просто использовать IRefAction<T>interfaceParallelHelper, которые будут автоматически загружать каждый элемент для передачи в наш коллбэк. Все, что необходимо, заключается в определении обратного 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 для инициализации всех элементов массива параллельно. Обратите внимание, как на этот раз мы напрямую захватываем целевой массив, и используем IActioninterface для обратного вызова метода, который мы используем, предоставляя текущий индекс параллельной итерации в качестве аргумента:

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# изменять значения его полей. Это относится только к полям экземпляра типа значения: если у обратного вызова есть поле struct любого типа или ссылочное поле static, то это значение будет правильно разделяться между параллельными потоками.

Методы

Это 4 основных API, предоставляемых ParallelHelper, соответствующих интерфейсам IAction, IAction2D, IRefAction<T> и IInAction<T>. Тип ParallelHelper также предоставляет ряд перегрузок для этих методов, которые предлагают несколько способов указать диапазоны итерации или тип обратного вызова ввода. For и For2D работают с IAction и IAction2D экземплярами, и они предназначены для использования, когда необходимо выполнить параллельную работу, которая не требует сопоставления с базовой коллекцией, которую можно напрямую обрабатывать с использованием индексов для каждой параллельной итерации. Перегрузки ForEach, вместо этого работают с экземплярами IRefAction<T> и IInAction<T>, и их можно использовать, когда параллельные итерации напрямую соответствуют элементам в коллекции, которая может быть индексирована напрямую. В этом случае они также абстрагируют логику индексирования, чтобы каждый параллельный вызов фокусировался только на входном элементе для обработки, а не на том, как его получить.

Примеры

Дополнительные примеры можно найти в модульных тестах.