【问题标题】:C# How Parallel.ForEach / Parallel.For partitioning worksC# Parallel.ForEach / Parallel.For 分区的工作原理
【发布时间】:2020-07-14 08:40:16
【问题描述】:

我有一些关于 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());
        }
    });

【问题讨论】:

标签: c# partitioning parallel.foreach


【解决方案1】:

对于Parallel.XYZ 的工作方式存在一些误解。

cmets中已经提到了几个重要的观点和建议,所以我不再赘述。相反,我想分享一些关于并行编程的想法。

平行类

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

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

Parallel 类支持这两种模型。 ForForEach 设计用于数据并行,而Invoke 用于任务并行。

分区

如果数据并行化,棘手的部分是如何对数据进行切片以获得最佳吞吐量/性能。您必须考虑数据收集的大小、数据的结构、处理逻辑和可用内核(以及许多其他方面)。所以没有一劳永逸的建议。

关于分区的主要问题是不要过度使用资源(一些核心空闲,而其他核心正在努力工作),也不要过度使用(等待作业比可用核心多得多,因此同步开销可以是重要)。

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

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

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

线程安全

如果每个执行器都可以执行其处理任务而无需与其他执行器交互,则它们被认为是独立的。如果您可以将算法设计为具有独立的处理单元,那么您可以最大限度地减少同步。

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

这就是为什么这些Parallel 方法有bodylocalFinally parameters 的原因。前者用于定义单个处理,而后者是聚合和合并功能。 (这有点类似于 Map-Reduce 方法)在后者中,您自己知道thread safety

PLINQ

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

有用的资源


编辑:如何确定值得并行运行?

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

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

这是我的指导方针:

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

详情

  1. 有几个非常好的工具可以帮助您深入了解您的应用程序。对于 .NET Profiling,我鼓励您尝试CodeTrack。如果不需要自定义指标,Concurrency Visualizer 也是不错的工具。
  2. 我所说的多次测量是指您应该使用几种不同的工具进行多次测量,以排除特殊情况。如果您只测量一次,那么您可能会得到假阳性结果。所以,测量两次,切割一次。
  3. 您的顺序处理应作为基线。基础过度并行化可能会导致某些开销,这就是为什么能够将您的新闪耀解决方案与当前解决方案进行比较是有意义的。利用率不足也会导致性能显着下降。
  4. 如果您可以提取有问题的代码,则可以执行微基准测试。我鼓励你看看很棒的 Benckmark.NET 工具来创建基准。
  5. 可以通过多种方式解决相同的问题。所以尝试找到几种不同的方法(比如 ParallelPLINQ 或多或少可以用于相同的问题)
  6. 正如我之前所说的测量,测量和测量。您还应该记住 .NET 尝试变得聪明。我的意思是例如AsParalleldoes not give you a guarantee that it will run in parallel。 .NET 分析您的解决方案和数据结构并决定如何运行它。另一方面,如果您确定它会有所帮助,您可以enforce parallel execution
  1. Scientist.NET 之类的库可以帮助您执行这种并行运行和比较过程的不足。
  1. 享受 :D

【讨论】:

  • 感谢您的回复。我只有一个问题,我们应该并行处理多少数据?比如说我有一个常见的例程,我传递一个 List 并且列表可能有小数据,而某些时间列表可能有大量数据。并行适用于大数据处理,但是当有小数据时,这也会减慢执行速度。所以我需要知道我应该为 Parallel 提供多少数据?最好用例子详细解释一下。谢谢
  • @Indi_Rain 我已经扩展了我的回复,请查看。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-17
相关资源
最近更新 更多