ParallelHelper

ParallelHelper 包含用于处理并行代码的高性能 API。 它包含以性能为导向的方法,可用于对给定数据集或迭代范围或区域快速设置和执行并行操作。

平台 API:ParallelHelperIActionIAction2DIRefAction<T>IInAction<T><T>

工作原理

ParallelHelper 类型基于三个主要概念:

  • 它针对目标迭代范围执行自动批处理。 这意味着,它根据可用的 CPU 内核数自动调度正确的工作单元数量。 这样做是为了减少每次并行迭代都要调用一次并行回调的开销。
  • 它在很大程度上利用泛型类型在 C# 中的实现方式,并使用实现特定接口的 struct 类型,而不是使用委托(如 Action<T>)。 这样做是为了使 JIT 编译器能够“查看”正在使用的每个单独回调类型,从而尽可能完全内联回调。 这可以大大减少每个并行迭代的开销,尤其是在使用非常小的回调时,这会仅对委托调用产生一些微不足道的成本。 此外,使用 struct 类型作为回调要求开发人员手动处理闭包中捕获的变量,从而防止从实例方法和其他值意外捕获 this 指针,这些方法和值可能会大幅减慢每个回调调用的速度。 这与其他以性能为导向的库(如 ImageSharp)中使用的方法相同。
  • 它公开 4 种类型的 API,这些 API 表示 4 种不同类型的迭代:1D 和 2D 循环、有副作用的项迭代和无副作用的项迭代。 每种类型的操作都有相应的 interface 类型,需要应用于传递给 ParallelHelper API 的 struct 回调:它们是 IActionIAction2DIRefAction<T>IInAction<T><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# 编译器还可以自动识别正在使用的类型参数,因此可以在方法调用中将它们一起忽略。

这种针对需要从回调访问的值创建字段的方法允许显式声明要捕获的值,这有助于使代码更具表现力。 这与在声明访问某些局部变量的 lambda 函数或本地函数时,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,因此它们会通过复制(而不是引用)传递给并行运行的每个线程。 这意味着,作为字段存储在回调类型中的值类型也将被复制。 记住此详细信息并避免错误的一个好做法是将回调 struct 标记为 readonly,以便 C# 编译器不会让我们修改其字段的值。 这仅适用于值类型的实例字段:如果回调 struct 包含任何类型的 static 字段或引用字段,则该值将在并行线程之间正确共享。

方法

这些是 ParallelHelper 公开的 4 个主要 API,分别对应 IActionIAction2DIRefAction<T>IInAction<T> 接口。 ParallelHelper 类型还公开了这些方法的多个重载,这些重载提供多种指定迭代范围或输入回调类型的方法。 ForFor2D 适用于 IActionIAction2D 实例,适合处理以下情况:需要完成某项不需要映射到基础集合的并行工作,而该集合可以使用每个并行迭代的索引直接访问。 而 ForEach 重载则适用于 IRefAction<T>IInAction<T> 实例,当并行迭代直接映射到可直接编制索引的集合中的项时,可以使用它们。 在这种情况下,它们还会抽象出索引逻辑,以便每个并行调用只需要关注要处理的输入项,而不必关注如何检索该项。

示例

可以在单元测试中查找更多示例。