Parallel.ForEach与Task.Run和Task.WhenAll的区别

240

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

版本 1:

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

第二版:

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);

5
如果你使用Task.WaitAll代替Task.WhenAll,我认为第二个代码片段几乎与第一个相同。 - avo
21
请注意,第二个例子将会执行三次 DoSomething("s3"),且结果与第一个例子不同! - Nullius
1
可能是Parallel.ForEach vs Task.Factory.StartNew的重复问题。 - Mohammad
@Dan:请注意,版本2使用了async/await,这意味着这是一个不同的问题。Async/await是在VS 2012中引入的,在可能重复的线程编写之后1.5年。 - Petter T
7
@Nullius,自C#5以来,捕获的变量表现出预期的方式,上面的循环对三个字符串中的每一个执行DoSomething,详见例如https://dev59.com/iGct5IYBdhLWcg3wa8s_。这个问题显然是针对C#5的,因为Task.WhenAll是在C#5中引入的,与.NET Framework 4.5一起发布。因此,第二个循环不会像所说的那样三次执行DoSomething("s3")。 - Petter T
显示剩余3条评论
4个回答

245

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

然而,在循环中使用Task.Run也有一个缺点- 使用Parallel.ForEach,系统会创建一个Partitioner来避免产生过多的任务。 Task.Run将始终为每个项创建单个任务(因为您正在执行此操作),但Parallel类会批量处理工作,因此您创建的任务数量少于总工作项数。这可以提供显着更好的整体性能,特别是如果循环体中每个项的工作量较小。

如果是这种情况,则可以通过编写以下内容来结合两种选项:

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

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

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

3
很棒的回答,不知道你能否推荐一些关于这个主题的好读物? - Dimitar Dimitrov
4
我的Parallel.ForEach结构导致我的应用程序崩溃。我在其中执行了一些重量级的图像处理操作。但当我添加Task.Run(()=> Parallel.ForEach(....));时,它就不再崩溃了。你能解释一下为什么吗?请注意,我将并行选项限制为系统上的核心数。 - monkeyjumps
1
@MonkeyJumps 因为 Parallel.ForEach 会阻塞当前线程,而 Task.Run 则使用异步框架允许线程继续运行而不阻塞。 - Josh M.
4
Õ”éµ×£DoSomethingµś»async void DoSomething’╝īõ╝ܵĆÄõ╣łµĀĘ’╝¤ - Francesco Bonizzi
1
async Task DoSomething 怎么样? - Shawn Mclean
显示剩余6条评论

52

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

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

还有使用的调度算法的差异。

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

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

3
应该改为 await Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)))); 对于返回任务而不是等待任务的语句,尤其是当涉及到像 using 这样的语句来释放对象时,会出现问题。 - Martín Coll
我的 Parallel.ForEach 调用导致了 UI 崩溃,我添加了 Task.Run(()=> Parallel.ForEach (.... ) ); 解决了崩溃问题。 - monkeyjumps
选项2在处理大量任务时,与选项1相比是否会给计算机增加额外的负担? - variable

1

我曾看到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.NewLine}Starting '{prefix}'...");
            action(prefix);
            Console.WriteLine($"{Environment.NewLine}Finished '{prefix}'{Environment.NewLine}");
        }
        

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

这是结果:

enter image description here

结论:

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


1
我认为这个结果是显而易见的,因为你从ForEach Body开始异步方法(即使用新线程池线程而不等待结果)。在这里我们必须调用DoSomethingAsync(i, prefix).Result。 - Mic
@Mic 尽管对你来说结果可能显而易见,但在 Web 应用程序中不适当地使用 Parallel.ForEach 的结果可能会导致服务器出现严重问题,这些问题直到应用程序承受负载才会出现。这篇文章并不是要说你不应该使用它,而是要确保那些使用它的人知道实际会发生什么。此外,你应该避免使用 .Result,因为你应该始终使用 async/await。 - Rogala
1
Parallel.ForEach 不能用于异步方法调用。由于 DoSomething 返回一个未被等待的任务,您应该在其上调用 .Wait()。现在,您将看到 Parallel.ForEach 只有在所有工作完成后才返回。 - Bouke
@Bouke,回答的重点是帮助那些不了解差异的人。话虽如此,您可以在Parallel.ForEach中使用任务,但它不会在主线程上执行。这并不意味着您应该这样做,但正如示例所示,代码中允许这种情况发生。这意味着任务中的代码正在另一个线程上执行,并且没有被阻塞。有些情况下,某人可能希望发生这种情况,但他们应该知道正在发生什么。 - Rogala
使用带有async委托的Parallel.ForEach是一个错误。其结果行为从未令人满意。正确的并行化异步工作的API是Parallel.ForEachAsync。该问题没有提到底层工作可以是异步的,因此这个答案是离题的,因此我会投反对票。 - Theodor Zoulias
@AndrewD.Bond 关于您的编辑(修订版3),使用Parallel.ForEachasync委托不同于调用未等待异步方法。前者是async voidfire-and-crash),后者是fire-and-forgetasync void委托不是任务。 - Theodor Zoulias

0

我最终选择了这种方式,因为它更易于阅读:

  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同时启动它们? - Vinicius Gualberto
1
据我所知,它们都是在我调用"DoSomethingAsync()"时启动的。但是,在调用WhenAll之前,没有任何阻塞。 - Chris M.
你的意思是第一次调用“DoSomethingAsync()”时吗? - Vinicius Gualberto
3
@ChrisM. 这段代码会被阻塞,直到第一个 DoSomethingAsync() 方法的 await 被执行,因为这个方法会将控制权交还给你的循环。如果代码是同步的,并且你返回了一个 Task,那么所有的代码都会按顺序执行,WhenAll 方法会等待所有任务完成。 - Simon Belanger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接