WhenAll vs WaitAll并行

时间:2014-12-17 13:58:56

标签: c# .net task-parallel-library async-await

我正在尝试了解WaitAllWhenAll如何运作并遇到以下问题。有两种方法可以从方法中获得结果:

  1. return Task.WhenAll(tasks).Result.SelectMany(r=> r);
  2. return tasks.Select(t => t.Result).SelectMany(r => r).ToArray();
  3. 如果我理解正确,第二种情况就像在WaitAll上调用tasks并在此之后获取结果一样。

    看起来第二种情况有更好的表现。我知道WhenAll的正确用法是使用await关键字,但我仍然想知道为什么这些行的性能存在很大差异。

    在分析了系统的流程后,我想我已经弄清楚如何在一个简单的测试应用程序中建模问题(测试代码基于I3arnon答案):

        public static void Test()
        {
            var tasks = Enumerable.Range(1, 1000).Select(n => Task.Run(() => Compute(n)));
    
            var baseTasks = new Task[100];
            var stopwatch = Stopwatch.StartNew();
            for (int i = 0; i < 100; i++)
            {
                baseTasks[i] = Task.Run(() =>
                {
                    tasks.Select(t => t.Result).SelectMany(r => r).ToList();
                });
    
            }
            Task.WaitAll(baseTasks);
            Console.WriteLine("Select - {0}", stopwatch.Elapsed);
    
            baseTasks = new Task[100];
            stopwatch.Restart();
            for (int i = 0; i < 100; i++)
            {
                baseTasks[i] = Task.Run(() =>
                {
                    Task.WhenAll(tasks).Result.SelectMany(result => result).ToList();
                });
    
            }
            Task.WaitAll(baseTasks);
            Console.WriteLine("Task.WhenAll - {0}", stopwatch.Elapsed);
        }
    

    看起来问题在于从其他任务(或Parallel循环)启动任务。在这种情况下,WhenAll会导致程序性能下降得多。那是为什么?

2 个答案:

答案 0 :(得分:5)

您正在Parallel.ForEach循环内启动任务,您应该避免。 Paralle.ForEach的重点是在可用CPU核心上并行化许多小但密集的计算,并且启动任务不是一项密集计算。相反,如果任务池已经饱和,它会创建一个任务对象并将其存储在队列中,并且很快就会启动1000个任务。所以现在Parallel.ForEach与任务池竞争计算资源。

在第一个非常慢的循环中,似乎调度不是最理想的,并且可能因为Task.WhenAll内的Parallel.ForEach而使用的CPU很少。如果您将Parallel.ForEach更改为正常for循环,您将看到加速。

但是,如果您的代码真的像Compute函数一样简单而没有在迭代之间进行任何状态,那么您可以摆脱任务并简单地使用Parallel.ForEach来最大化性能:

Parallel.For(0, 100, (i, s) =>
{
    Enumerable.Range(1, 1000).Select(n => Compute(n)).SelectMany(r => r).ToList();
});

至于为什么Task.WhenAll执行得更糟,你应该意识到这段代码

tasks.Select(t => t.Result).SelectMany(r => r).ToList();

不会并行运行任务。 ToList基本上将迭代包装在foreach循环中,并且循环体创建一个任务,然后等待任务完成,因为您检索Task.Result属性。因此,循环的每次迭代都将创建一个任务,然后等待它完成。 1000个任务一个接一个地执行,处理任务的开销很小。这意味着您不需要我上面建议的任务。

另一方面,代码

Task.WhenAll(tasks).Result.SelectMany(result => result).ToList();

将启动所有任务并尝试同时执行它们,因为任务池无法并行执行1000个任务,大多数这些任务在执行之前都会排队。这会产生很大的管理和任务切换开销,从而解释了糟糕的性能。

关于你添加的最后一个问题:如果外部任务的唯一目的是启动内部任务,那么外部任务没有用处,但如果外部任务是在那里执行某种内部协调任务然后它可能有意义(也许你想将Task.WhenAnyTask.WhenAll结合起来)。没有更多的背景,很难回答。但是,您的问题似乎与性能有关,启动100,000个任务可能会增加相当大的开销。

如果您想要像在示例中那样执行100,000次独立计算,那么

Parallel.ForEach是一个不错的选择。任务非常适合执行并发活动,这些活动涉及对您想要等待并组合结果并处理错误的其他系统的“慢速”调用。对于大规模的并行性,它们可能不是最佳选择。

答案 1 :(得分:2)

你的考试太复杂了,所以我自己做了。这是一个包含Consume方法的简单测试:

public static void Test()
{
    var tasks = Enumerable.Repeat(int.MaxValue, 10000).Select(n => Task.Run(() => Compute(n)));

    var stopwatch = Stopwatch.StartNew();
    Task.WhenAll(tasks).Result.SelectMany(result => result).ToList();
    Console.WriteLine("Task.WhenAll - {0}", stopwatch.Elapsed);

    stopwatch.Restart();
    tasks.Select(t => t.Result).SelectMany(r => r).ToList();
    Console.WriteLine("Select - {0}", stopwatch.Elapsed);
}

private static List<int> Compute(int seed)
{
    var results = new List<int>();
    for (int i = 0; i < 5000; i++)
    {
        results.Add(seed * i);
    }

    return results;
}

输出:

Task.WhenAll - 00:00:01.2894227
Select - 00:00:01.7114142

但是,如果我使用Enumerable.Repeat(int.MaxValue, 100),则输出为:

Task.WhenAll - 00:00:00.0205375
Select - 00:00:00.0178089

基本上,选项之间的区别在于您是阻止一次还是阻止每个元素。当存在许多元素时阻塞一次会更好,但是对于每个元素来说阻塞很少可能会更好。

由于确实存在很大差异,并且只关注处理多个项目并且在完成所有任务时你想要继续处理时我只关心性能我建议使用Task.WhenAll