Parallel.ForEach 与 Task.Run 和 Task.WhenAll

Posted

技术标签:

【中文标题】Parallel.ForEach 与 Task.Run 和 Task.WhenAll【英文标题】:Parallel.ForEach vs Task.Run and Task.WhenAll 【发布时间】:2013-10-06 19:58:54 【问题描述】:

使用 Parallel.ForEach 或 Task.Run() 异步启动一组任务有什么区别?

版本 1:

List<string> strings = new List<string>  "s1", "s2", "s3" ;
Parallel.ForEach(strings, s =>

    DoSomething(s);
);

版本 2:

List<string> strings = new List<string>  "s1", "s2", "s3" ;
List<Task> Tasks = new List<Task>();
foreach (var s in strings)

    Tasks.Add(Task.Run(() => DoSomething(s)));

await Task.WhenAll(Tasks);

【问题讨论】:

如果您使用Task.WaitAll 而不是Task.WhenAll,我认为第二个代码片段几乎等于第一个。 另请注意,第二个将执行 DoSomething("s3") 三次,它不会产生相同的结果! ***.com/questions/4684320/… Parallel.ForEach vs Task.Factory.StartNew的可能重复 @Dan:请注意,版本 2 使用 async/await,这意味着这是一个不同的问题。 Async/await 是在 VS 2012 中引入的,在编写可能的重复线程 1.5 年后。 @Nullius,从 C#5 开始,捕获的变量的行为符合预期,上面的循环对三个字符串中的每一个执行 DoSomething,例如***.com/questions/12112881/…。这个问题显然是针对 C#5 的,因为 Task.WhenAll 是在 C#5 和 .NET Framework 4.5 中引入的。所以第二个执行 DoSomething("s3") 三次是不正确的。 【参考方案1】:

第一个版本将同步阻塞调用线程(并在其上运行一些任务)。 如果是 UI 线程,这将冻结 UI。

第二个版本将在线程池中异步运行任务并释放调用线程,直到它们完成。

使用的调度算法也有区别。

请注意,您的第二个示例可以缩短为

await Task.WhenAll(strings.Select(s => Task.Run(() => DoSomething(s))));

【讨论】:

不应该是await Task.WhenAll(strings.Select(async s =&gt; await Task.Run(() =&gt; DoSomething(s)));吗?我在返回任务(而不是等待)时遇到了问题,尤其是当涉及像 using 这样的语句来处理对象时。 我的 Parallel.ForEach 调用导致我的 UI 崩溃我添加了 Task.Run(()=> Parallel.ForEach (....) );到它,它解决了崩溃。 对于大量任务,选项 2 是否比选项 1 增加了额外的计算机开销?【参考方案2】:

在这种情况下,第二种方法将异步等待任务完成而不是阻塞。

但是,在循环中使用Task.Run 有一个缺点——使用Parallel.ForEach,会创建一个Partitioner,以避免执行不必要的任务。 Task.Run 将始终为每个项目创建一个任务(因为您正在这样做),但 Parallel 类批处理工作,因此您创建的任务比总工作项目少。这可以提供显着更好的整体性能,尤其是在循环体每个项目的工作量很小的情况下。

如果是这种情况,您可以通过以下方式组合这两个选项:

await Task.Run(() => Parallel.ForEach(strings, s =>

    DoSomething(s);
));

请注意,这也可以写成更短的形式:

await Task.Run(() => Parallel.ForEach(strings, DoSomething));

【讨论】:

很好的答案,我想知道你是否可以为我指出关于这个主题的好的阅读材料? 我的 Parallel.ForEach 构造使我的应用程序崩溃。我在里面进行了一些繁重的图像处理。但是,当我添加 Task.Run(()=> Parallel.ForEach(....));它停止了崩溃。你能解释一下为什么吗?请注意,我将并行选项限制为系统上的内核数。 如果DoSomethingasync void DoSomething 怎么办? async Task DoSomething 呢? @ShawnMclean - 您可以将异步添加为:await Task.Run(() => Parallel.ForEach(strings, async s => await DoSomething(s); ));【参考方案3】:

我最终这样做了,因为它更容易阅读:

  List<Task> x = new List<Task>();
  foreach(var s in myCollectionOfObject)
  
      // Note there is no await here. Just collection the Tasks
      x.Add(s.DoSomethingAsync());
  
  await Task.WhenAll(x);

【讨论】:

这样,您正在执行的任务是一个接一个地执行,还是WhenAll 一次启动所有任务? 据我所知,它们都是在我调用“DoSomethingAsync()”时启动的。然而,在调用 WhenAll 之前,没有任何东西可以阻止它们。 你的意思是当第一个“DoSomethingAsync()”被调用的时候? @ChrisM。它将被阻塞,直到第一次等待 DoSomethingAsync() 因为这会将执行转移回您的循环。如果它是同步的并且您返回一个任务,则所有代码将一个接一个地运行,并且 WhenAll 将等待所有任务完成【参考方案4】:

我看到 Parallel.ForEach 使用不当,我认为这个问题中的一个例子会有所帮助。

当您在控制台应用程序中运行以下代码时,您将看到在 Parallel.ForEach 中执行的任务如何不会阻塞调用线程。如果您不关心结果(正面或负面),这可能没问题,但如果您确实需要结果,则应确保使用 Task.WhenAll。

using System;
using System.Linq;
using System.Threading.Tasks;

namespace ParrellelEachExample

    class Program
    
        static void Main(string[] args)
        
            var indexes = new int[]  1, 2, 3 ;

            RunExample((prefix) => Parallel.ForEach(indexes, (i) => DoSomethingAsync(i, prefix)),
                "Parallel.Foreach");

            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("*You'll notice the tasks haven't run yet, because the main thread was not blocked*");
            Console.WriteLine("Press any key to start the next example...");
            Console.ReadKey();
            
            RunExample((prefix) => Task.WhenAll(indexes.Select(i => DoSomethingAsync(i, prefix)).ToArray()).Wait(),
                "Task.WhenAll");
            Console.WriteLine("All tasks are done.  Press any key to close...");
            Console.ReadKey();
        

        static void RunExample(Action<string> action, string prefix)
        
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine($"Environment.NewLineStarting 'prefix'...");
            action(prefix);
            Console.WriteLine($"Environment.NewLineFinished 'prefix'Environment.NewLine");
        
        

        static async Task DoSomethingAsync(int i, string prefix)
        
            await Task.Delay(i * 1000);
            Console.WriteLine($"Finished: prefix[i]");
        
    

结果如下:

结论:

将 Parallel.ForEach 与 Task 一起使用不会阻塞调用线程。如果您关心结果,请确保等待任务。

【讨论】:

我认为这个结果很明显,因为您从 ForEach Body 启动 Async 方法(即使用新的 ThreadPool 线程而不等待结果)。这里我们必须调用 DoSomethingAsync(i, prefix).Result。 @Mic 虽然结果对您来说似乎很明显,但在 Web 应用程序中不当使用 Parallel.ForEach 的结果可能会导致服务器中出现一些严重问题,这些问题在加载到应用程序。这篇文章并不是说你不应该使用它,而是要确保那些使用它的人知道实际会发生什么。此外,您应该避免使用 .Result,因为您应该始终使用 async/await。 Parallel.ForEach 不能用于异步方法调用。由于DoSomething 返回了一个未等待的任务,您应该在其上调用.Wait()。现在您将看到 Parallel.ForEach 仅在所有工作完成后才返回。 @Bouke 答案的重点是帮助那些不了解差异的人。也就是说,您可以在 Parallel.ForEach 中使用任务,但它不会在主线程上执行。这并不意味着您应该这样做,但正如示例所演示的那样,它在代码中是允许的。这意味着任务中的代码在不同的线程上执行并且没有被阻塞。在某些情况下,有人可能希望发生这种情况,但他们应该知道正在发生的事情。

以上是关于Parallel.ForEach 与 Task.Run 和 Task.WhenAll的主要内容,如果未能解决你的问题,请参考以下文章

Parallel.ForEach 与 Task.Factory.StartNew

Task.StartNew() 与 Parallel.ForEach :多个 Web 请求场景

在 .NET 3.5 中将 Parallel.Foreach 与分区器一起使用

Asp.Net 中有没有办法与运行 Parallel.Foreach 的后台线程进行通信

Parallel.ForEach 之 MaxDegreeOfParallelism

由于 HttpClient 请求缓慢,Task.Result 在 Parallel.ForEach 内阻塞