【问题标题】:ParallelEnumerable.Aggregate for several methodsParallelEnumerable.Aggregate 用于几种方法
【发布时间】:2017-08-18 10:28:56
【问题描述】:

开始学习多线程。有 3 种方法来计算数组平方根的和、平均值和乘积。

首先,我使用 PLINQ 进行了三个单独的阻塞调用。然后我认为能够在一次调用中完成它并同时返回一个包含总和、乘积和平均值的对象会很好。我读到 ParallelEnumerable.Aggregate 可以帮助我解决这个问题,但我完全不知道如何使用它。

如果我能解释一下如何在我的情况下使用此功能,我将非常感谢,以及这种方法的好/坏方面。

public static double Average(double[] array, string tool)
        {
            if (array == null) throw new ArgumentNullException(nameof(array));
            double sum = Enumerable.Sum(array);
            double result = sum / array.Length;
            Print(tool, result);
            return result;
        }

        public static double Sum(double[] array, string tool)
        {
            if (array == null) throw new ArgumentNullException(nameof(array));
            double sum = Enumerable.Sum(array);
            Print(tool, sum);
            return sum;
        }

        public static void ProductOfSquareRoots(double[] array, string tool)
        {
            if (array == null) throw new ArgumentNullException(nameof(array));
            double result = 1;
            foreach (var number in array)
            {
                result = result * Math.Sqrt(number);
            }
            Print(tool, result);
        }

【问题讨论】:

    标签: c# .net multithreading parallel-processing plinq


    【解决方案1】:

    您要计算的三个聚合值(平均值、总和和平方根的乘积)都可以通过对数字执行一次遍历来计算。您可以这样做一次并在循环内聚合三个值,而不是这样做三次(每个聚合值一次)。

    平均值是总和除以计数,由于您已经在计算总和,因此您只需要计数即可获得平均值。如果您知道输入的大小,您甚至不必计算项目,但在这里我假设输入的大小事先是未知的。

    如果你想使用 LINQ 你可以使用Aggregate:

    var aggregate = numbers.Aggregate(
        // Starting value for the accumulator.
        (Count: 0, Sum: 0D, ProductOfSquareRoots: 1D),
        // Update the accumulator with a specific number.
        (accumulator, number) =>
        {
            accumulator.Count += 1;
            accumulator.Sum += number;
            accumulator.ProductOfSquareRoots *= Math.Sqrt(number);
            return accumulator;
        });
    

    变量aggregate 是一个ValueTuple<int, double, double>,其中包含CountSumProductOfSquareRoots 项。在 C# 7 之前,您将使用匿名类型。但是,这将需要为输入序列中的每个值进行分配,从而减慢聚合速度。通过使用可变值元组,聚合应该会变得更快。

    Aggregate 与 PLINQ 一起使用,因此如果 numbers 的类型为 ParallelQuery<T> 而不是 IEnumerable<T>,则聚合将并行执行。请注意,这要求聚合既是关联的(例如 (a + b) + c = a + (b + c) 和交换的(例如 a + b = b + a),在您的情况下是正确的。

    PLINQ 有开销,因此与单线程 LINQ 相比,它的性能可能不会更好,具体取决于序列中的元素数量和计算的复杂程度。您必须自己测量这一点,以确定 PLINQ 是否加快了速度。但是,您可以在 LINQ 和 PLINQ 中使用相同的 Aggregate 表达式,通过在正确的位置插入 AsParallel(),您的代码可以轻松地从单线程切换到并行。

    【讨论】:

      【解决方案2】:

      注意:你必须将result变量初始化为1,否则你将永远得到0。

      注意2:不要写Enumerable.Sum(array),而是写array.Sum()

      不,Aggregate 方法不会帮助您同时计算三个函数。请参阅 Martin Liversage 的答案。


      KISS ;)

      if (array == null) throw new ArgumentNullException(nameof(array));
      
      var sum = array.Sum();
      var average = array.Average();
      var product = array.Aggregate(1.0, (acc, val) => acc * Math.Sqrt(val));
      

      可以简化:

      var average = sum / array.Length;
      

      这消除了通过数组的额外通道。


      想要并行化?

      var sum = array.AsParallel().Sum();
      //var average = array.AsParallel().Average(); // Extra pass!
      var average = sum / array.Length; // More fast! Really!
      var product = array.AsParallel().Aggregate(1.0, (acc, val) => acc * Math.Sqrt(val));
      

      但是,它可能会比以前的方法慢。这种并行只适用于非常大的集合,包含数十亿个元素。


      每次通过集合都需要时间。通过次数越少,性能越好。在计算平均值时,我们已经处理了一个。让我们只做一个。

      double sum = 0;
      double product = 1;
      
      foreach (var number in array)
      {
          sum += number;
          product = product * Math.Sqrt(number);
      }
      
      double average = sum / array.Length;
      

      一次过三个结果!我们是最棒的!


      让我们回到主题。

      Parallel.Invoke 方法允许您并行执行多个函数,但它不会从中获取结果。适用于“一劳永逸”类型的计算。

      我们可以通过运行多个任务来并行计算。在Task.WhenAll 的帮助下等待他们全部完成并得到结果。

      var results = await Task.WhenAll(
              Task.Run(() => array.Sum()),
              Task.Run(() => array.Average()),
              Task.Run(() => array.Aggregate(1.0, (acc, val) => acc * Math.Sqrt(val)))
          );
      
      var sum = results[0];
      var average = results[1];
      var product = results[2];
      

      对于小型收藏也无效。但在某些情况下,它可能比AsParallel 更有效。

      用任务编写这种方法的另一种方式。也许它会看起来更清楚。

      var sumTask = Task.Run(() => array.Sum());
      var avgTask = Task.Run(() => array.Average());
      var prodTask = Task.Run(() => array.Aggregate(1.0, (acc, val) => acc * Math.Sqrt(val)));
      
      Task.WaitAll(sumTask, avgTask, prodTask);
      
      sum = sumTask.Result;
      average = avgTask.Result;
      product = prodTask.Result;
      

      【讨论】:

      • 很好的解释!谢谢!
      猜你喜欢
      • 2010-12-26
      • 2018-05-28
      • 1970-01-01
      • 1970-01-01
      • 2017-06-27
      • 2013-02-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多