Parallel.ForEach与Task.Factory.StartNew


267

以下代码段之间有什么区别?不会都使用线程池线程吗?

例如,如果我想为集合中的每个项目调用一个函数,

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}

Answers:


302

第一个是更好的选择。

在内部,Parallel.ForEach使用a Partitioner<T>将您的集合分发到工作项中。它不会为每个项目执行一项任务,而是分批执行此任务以降低所涉及的开销。

第二个选项将安排一个 Task您的收藏中的每个项目。尽管结果(几乎)是相同的,但是这将带来不必要的额外开销,尤其是对于大型集合而言,这将导致整体运行时间变慢。

仅供参考- 如果需要,可以通过对Parallel.ForEach使用适当的重载来控制使用的Partitioner 。有关详细信息,请参阅MSDN上的“ 自定义分区程序 ”。

在运行时的主要区别是第二个将异步执行。可以使用Parallel.ForEach通过执行以下操作来复制它:

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

这样,您仍然可以利用分区程序,但是在操作完成之前不要阻塞。


8
IIRC是Parallel.ForEach进行的默认分区,它还考虑了可用的硬件线程数,从而使您不必计算出要启动的最佳任务数。查看Microsoft的“并行编程模式”一文;它对所有这些东西都有很好的解释。
马尔罗斯,

2
@Mal:有点……实际上不是Partitioner,而是TaskScheduler的工作。默认情况下,TaskScheduler使用新的ThreadPool,现在可以很好地处理此问题。
Reed Copsey

谢谢。我知道我应该离开“我不是专家,但是...”警告。:)
Mal Ross

@ReedCopsey:如何将通过Parallel.ForEach启动的任务附加到包装器任务?这样,当您在包装任务上调用.Wait()时,它会挂起,直到并行运行的任务完成为止?
康斯坦丁·塔尔库斯

1
@Tarkus如果要发出多个请求,最好在每个工作项中(在Parallel循环中)使用HttpClient.GetString。没有理由将异步选项放入已经存在的并发循环中,通常是……
Reed Copsey

89

我做了一个小实验,使用“ Parallel.For”和“ Task”对象运行方法“ 1,000,000,000(十亿)”次。

我测量了处理器时间,发现并行效率更高。Parallel.For将您的任务划分为多个小的工作项,并以最佳方式在所有内核上并行执行它们。在创建许多任务对象时(FYI TPL将在内部使用线程池)将移动每个任务的每次执行,从而在框中产生更大的压力,这从下面的实验中可以明显看出。

我还制作了一个小视频,解释了基本的TPL,还演示了Parallel.For如何更有效地利用您的内核http://www.youtube.com/watch?v=No7QqSc5cl8与常规任务和线程相比。

实验1

Parallel.For(0, 1000000000, x => Method1());

实验2

for (int i = 0; i < 1000000000; i++)
{
    Task o = new Task(Method1);
    o.Start();
}

处理器时间比较


这样会更有效,并且创建线程的成本很高的原因实验2是非常不好的做法。
蒂姆(Tim)

@ Georgi-it请关心更多谈论坏处。
Shivprasad Koirala 2014年

3
对不起,我的错误,我应该澄清。我的意思是在1000000000循环中创建Tasks。开销是无法想象的。更不用说Parallel一次不能创建63个以上的任务,这使其在这种情况下更加优化。
Georgi-it

对于1000000000个任务,这是正确的。但是,当我处理图像(反复地,进行分形缩放)并执行Parallel.For时,在等待最后一个线程完成时,许多内核处于空闲状态。为了使其更快,我自己将数据细分为64个工作包,并为其创建了任务。(然后Task.WaitAll等待完成。)这个想法是让空闲线程拾取一个工作包来帮助完成工作,而不是等待1-2个线程完成其(Parallel.For)分配的块。
Tedd Hansen

1
Mehthod1()在此示例中做什么?
Zapnologica

17

Parallel.ForEach将优化(甚至可能不会启动新线程)并阻塞直到循环完成,而Task.Factory将为每个项目显式创建一个新的任务实例,并在它们完成之前返回(异步任务)。Parallel.Foreach效率更高。


11

在我看来,最现实的情况是任务需要完成繁重的操作。Shivprasad的方法更多地关注对象创建/内存分配,而不是计算本身。我进行了一项研究,研究了以下方法:

public static double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result; 
}

执行此方法大约需要0.5秒。

我使用Parallel调用了200次:

Parallel.For(0, 200, (int i) =>
{
    SumRootN(10);
});

然后我用老式的方法调用了200次:

List<Task> tasks = new List<Task>() ;
for (int i = 0; i < loopCounter; i++)
{
    Task t = new Task(() => SumRootN(10));
    t.Start();
    tasks.Add(t);
}

Task.WaitAll(tasks.ToArray()); 

第一起案件在26656毫秒内完成,第二起案件在24478毫秒内完成。我重复了很多次。每当第二种方法稍快一些时。


使用Parallel.For是老式的方法。对于不统一的工作单元,建议使用“任务”。微软MVP和TPL的设计人员还提到,使用任务将更有效地使用线程,即在等待其他单元完成时不会阻塞太多线程。
Suncat2000
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.