Поделиться через


ParallelHelper

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

API платформы: ParallelHelper, IAction, IAction2DIRefAction<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> IActionIAction2Dинтерфейсам и IInAction<T> интерфейсам. Тип ParallelHelper также предоставляет ряд перегрузок для этих методов, которые предлагают несколько способов указать диапазоны итерации или тип обратного вызова ввода. For и For2D работа над IAction и IAction2D экземплярами, и они предназначены для использования, когда необходимо выполнить параллельную работу, которая не требует сопоставления с базовой коллекцией, к которым можно напрямую обращаться с индексами каждой параллельной итерации. Перегрузки ForEach , а не wotk в IRefAction<T> и IInAction<T> экземплярах, и их можно использовать, когда параллельные итерации напрямую сопоставляются с элементами в коллекции, которая может быть индексирована напрямую. В этом случае они также абстрагируют логику индексирования, чтобы каждый параллельный вызов должен беспокоиться только о входном элементе для работы, а не о том, как получить этот элемент.

Примеры

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