C# Parallel.ForEach / Parallel.For 分区如何工作

C# How Parallel.ForEach / Parallel.For partitioning works

我有一些关于 Parallel.ForEach 使用分区方法 的基本问题,我遇到了一些问题,所以我想了解这段代码的工作原理和它的流程是什么。

代码示例

var result = new StringBuilder();
Parallel.ForEach(Enumerable.Range(1, 5), () => new StringBuilder(), (x, option, sb) =>
{
    sb.Append(x);
    return sb;
}, sb =>
{
    lock (result)
    {
        result.Append(sb.ToString());
    }
});

上面代码相​​关问题:

  1. 他们是否在并行 foreach 中进行一些分区工作

  2. 当我调试代码时,我可以看到代码的迭代(执行)发生了 5 次以上,但据我了解它应该只触发 5 次 - Enumerable.Range(1, 5).

  3. 这段代码什么时候触发?在 Parallel.ForeachParallel.For 中,都有两个由 {} 分隔的块。这两个块如何执行并相互交互?

    lock (result)
    {
       result.Append(sb.ToString());
    }

奖金问题:

查看此代码块,其中没有发生 5 次迭代,而是发生了更多迭代。当我使用 Parallel For 而不是 foreach 时。查看代码并告诉我哪里出错了。

    var result = new StringBuilder();
    Parallel.For(1, 5, () => new StringBuilder(), (x, option, sb) =>
    {
        sb.Append("line " + x + System.Environment.NewLine);
        MessageBox.Show("aaa"+x.ToString());
        return sb;
        
    }, sb =>
    {
        lock (result)
        {
            result.Append(sb.ToString());
        }
    });

关于 Parallel.XYZ 的工作原理存在一些误解。

评论中已经提到了一些很好的观点和建议,我就不再重复了。相反,我想分享一些关于并行编程的想法。

并行Class

每当我们谈论并行编程时,我们通常会区分两种:数据并行任务并行。前者在一大块数据上并行执行相同的函数。后者并行执行几个独立的功能。

(还有第三种模型称为管道,它是这两者的混合体。如果您对此感兴趣,我不会花时间在上面,我建议您搜索 Task Parallel Library's Dataflow or System.Threading.Channels。 )

Parallel class 支持这两种模型。 ForForEach 是为数据并行设计的,而 Invoke 是为任务并行设计的。

分区

在数据并行的情况下,棘手的部分是如何分割数据以获得最佳吞吐量/性能。您必须考虑数据集合的大小、数据结构、处理逻辑和可用核心(以及许多其他方面)。所以没有一刀切的建议。

分区的主要问题是不要使用不足的资源(一些内核空闲,而另一些内核正在努力工作),也不要过度使用(等待作业的数量远远多于可用内核,因此同步开销可能很重要)。

假设您的处理逻辑非常稳定(换句话说,各种输入数据不会显着改变处理时间)。在这种情况下,您可以对执行程序之间的数据进行负载平衡。如果执行程序完成,那么它可以获取要处理的新数据。

您可以通过 Partitioner(1) 来定义您如何选择哪些数据应该发送给哪个执行者的方式。默认情况下.NET 支持Range、Chunk、Hash 和Striped 分区。有些是静态的(分区在任何处理之前完成),有些是动态的(取决于处理速度,一些执行者可能比其他执行者接收更多)。

以下两篇优秀文章可以让您更好地了解每个分区的工作原理:

线程安全

如果每个执行者都可以在不需要与他人交互的情况下执行其处理任务,那么他们就被认为是独立的。如果您可以将算法设计为具有独立的处理单元,则可以最大限度地减少同步。

ForForEach 的情况下,每个分区都可以有自己的分区本地存储。这意味着计算是独立的,因为中间结果存储在分区感知存储中。但像往常一样,您想将它们合并到一个集合中,甚至合并到一个值中。

这就是为什么这些Parallel方法自己有bodylocalFinallyparameters. The former is used to define the individual processing, while the latter is the aggregate and merge function. (It is kinda similar to the Map-Reduce approach) In the latter you have aware of thread safety的原因。

PLINQ

我不想探讨这个话题,这超出了问题的范围。但我想给你一个从哪里开始的缺口:

有用的资源


编辑:如何决定并行 运行 的价值?

没有单一的公式(至少据我所知)可以告诉您何时使用并行执行是有意义的。正如我在“分区”部分中试图强调的那样,这是一个非常复杂的主题,因此需要多次实验和微调才能找到最佳解决方案。

我强烈建议您测量并尝试几种不同的设置。

这是我的指南,你应该如何解决这个问题:

  1. 尝试了解您的应用程序的当前特征
  2. 执行几个不同的测量来发现执行瓶颈
  3. 捕获当前解决方案的性能指标作为基准
  4. 如果可能的话,尝试从代码库中提取那段代码以方便微调
  5. 尝试从几个不同的方面和不同的输入来解决同一个问题
  6. 测量它们并将它们与您的基线进行比较
  7. 如果您对结果感到满意,那么将那段代码放入您的代码库并在不同的工作负载下再次测量
  8. 尽可能多地获取相关指标
  9. 如果可以考虑同时执行(顺序和并行)解决方案并比较它们的结果。
  10. 如果满意就把顺序码去掉

详情

  1. 有几个非常好的工具可以帮助您深入了解您的应用程序。对于 .NET 分析,我鼓励您尝试使用它 CodeTrack. Concurrency Visualizer 如果不需要自定义指标,它也是一个很好的工具。
  2. 我所说的多次测量是指您应该使用几种不同的工具进行多次测量,以排除特殊情况。如果您只测量一次,那么您可能会得到假阳性结果。所以,测量两次,切割一次。
  3. 您的顺序处理应作为基准。基础过度并行化会导致一定的开销,这就是为什么能够将您的新 Shine 解决方案与当前解决方案进行比较是有意义的。利用率不足也会导致性能显着下降。
  4. 如果您可以提取有问题的代码,那么您就可以执行微基准测试。我鼓励您看看很棒的 Benckmark.NET 工具来创建基准。
  5. 同一个问题可以有多种解决方法。所以尝试找到几种不同的方法(比如ParallelPLINQ可以或多或少地用于相同的问题)
  6. 正如我之前所说的衡量,衡量和衡量。您还应该牢记 .NET 尽量聪明。我的意思是,例如 AsParallel does not give you a guarantee that it will run in parallel. .NET analysis your solution and data structure and decide how to run it. On the other hand you can enforce parallel execution 如果您确定它会有所帮助。
  1. 有像 Scientist.NET 这样的库可以帮助您执行这个简短的并行 运行 和比较过程。
  1. 享受:D