Parallel.ForEach与Task.Run和Task.WhenAll


158

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

3
我认为,如果您使用Task.WaitAll而不是,则第二个代码片段将几乎等于第一个代码片段Task.WhenAll
13年

15
另请注意,第二个将执行三次DoSomething(“ s3”),并且不会产生相同的结果! stackoverflow.com/questions/4684320/...
无主地


@Dan:请注意,版本2使用async / await,这意味着这是一个不同的问题。在写入可能的重复线程1.5年后,VS 2012引入了Async / await。
Petter T

Answers:


159

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

但是,Task.Run在循环中使用具有一个缺点-With Parallel.ForEachPartitioner它的创建是为了避免执行不必要的任务。 Task.Run(因为您正在执行此操作)将始终为每个项目执行单个任务,但是Parallel类批次可以工作,因此您创建的任务少于总工作项。这可以提供明显更好的整体性能,尤其是在循环体每个项目的工作量较小的情况下。

在这种情况下,您可以通过编写以下两个选项来组合:

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

请注意,这也可以用以下简短形式编写:

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

1
很好的答案,我想知道您是否可以为我提供有关该主题的优质阅读材料?
Dimitar Dimitrov 2014年

@DimitarDimitrov对于一般的TPL内容,reedcopsey.com / series / parallelism
Reed Copsey

1
我的Parallel.ForEach构造使我的应用程序崩溃。我正在其中进行一些沉重的图像处理。但是,当我添加Task.Run(()=> Parallel.ForEach(....)); 它停止崩溃了。你能解释为什么吗?请注意,我将并行选项限制为系统上的内核数。
monkeyjumps 2014年

3
如果DoSomethingasync void DoSomething什么呢?
Francesco Bonizzi '16

1
async Task DoSomething
肖恩·麦克林

37

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

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

使用的调度算法也存在差异。

请注意,您的第二个示例可以简化为

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

2
不是await Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)));吗?当返回任务(而不是等待)时,我遇到了问题,尤其是当using涉及诸如处理对象的语句时。
马丁·科尔

我的Parallel.ForEach调用导致UI崩溃,我添加了Task.Run(()=> Parallel.ForEach(....)); 到它,它解决了崩溃。
monkeyjumps 2014年

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

据我所知,它们全部在我调用“ DoSomethingAsync()”时启动。但是,直到调用WhenAll之前,都没有任何障碍。
克里斯·M

您的意思是何时调用第一个“ DoSomethingAsync()”?
Vinicius Gualberto

1
@ChrisM。它将一直被阻塞,直到第一次等待DoSomethingAsync()为止,因为这会将执行转移回您的循环中。如果它是同步的并且您返回一个Task,则所有代码将一个接一个地运行,并且WhenAll将等待所有Task完成
Simon Belanger

0

我看过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}]");
        }
    }
}

结果如下:

在此处输入图片说明

结论:

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

〜干杯

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.