ParallelEnumerable.Aggregate用于几种方法

时间:2017-08-18 10:28:56

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

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

首先,我使用PLINQ进行三次单独的阻止调用。然后我认为能够在一次调用中创建它并同时返回一个sum,product和average的对象会很好。我读到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);
        }

2 个答案:

答案 0 :(得分:3)

您想要计算的三个聚合值(平均值,平方和的乘积和乘积)可以通过对数字执行单次传递来计算。您可以执行此操作一次并在循环内聚合三个值(这可以节省时间),而不是这样做三次(每个聚合值一次)。

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

如果您想使用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;
    });

变量aggregateValueTuple<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()插入正确的位置,可以轻松地将代码从单线程切换到并行。

答案 1 :(得分:2)

注意:您必须使用值1初始化result变量,否则您将始终获得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;