Parallel.ForEach 与 Task.Factory.StartNew
Posted
技术标签:
【中文标题】Parallel.ForEach 与 Task.Factory.StartNew【英文标题】:Parallel.ForEach vs Task.Factory.StartNew 【发布时间】:2011-06-27 21:47:11 【问题描述】:下面的代码sn-ps有什么区别?两者都不会使用线程池线程吗?
例如,如果我想为集合中的每个项目调用一个函数,
Parallel.ForEach<Item>(items, item => DoSomething(item));
vs
foreach(var item in items)
Task.Factory.StartNew(() => DoSomething(item));
【问题讨论】:
【参考方案1】:第一个是更好的选择。
Parallel.ForEach 在内部使用Partitioner<T>
将您的集合分发到工作项中。它不会对每个项目执行一项任务,而是将其批处理以降低所涉及的开销。
第二个选项将为您的收藏中的每个项目安排一个Task
。虽然结果(几乎)相同,但这会带来比必要更多的开销,尤其是对于大型集合,并导致整体运行时间变慢。
仅供参考 - 如果需要,可以使用适当的 overloads to Parallel.ForEach 来控制所使用的分区器。有关详细信息,请参阅 MSDN 上的Custom Partitioners。
在运行时,主要区别在于第二个将异步执行。这可以使用 Parallel.ForEach 复制:
Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));
通过这样做,您仍然可以利用分区器,但在操作完成之前不要阻塞。
【讨论】:
IIRC,由 Parallel.ForEach 完成的默认分区也考虑了可用的硬件线程数,从而使您不必计算最佳的任务数来启动。查看微软的Patterns of Parallel Programming 文章;它对所有这些东西都有很好的解释。 @Mal:有点……这实际上不是分区器,而是任务调度器的工作。默认情况下,TaskScheduler 使用新的 ThreadPool,它现在可以很好地处理这个问题。 谢谢。我知道我应该离开“我不是专家,但是……”警告。 :) @ReedCopsey:如何将通过 Parallel.ForEach 启动的任务附加到包装任务?这样当您在包装任务上调用 .Wait() 时,它会挂起,直到并行运行的任务完成? @Tarkus 如果您发出多个请求,最好在每个工作项(在您的并行循环中)中使用 HttpClient.GetString。没有理由在已经并发的循环中放置异步选项,通常...【参考方案2】:我做了一个小实验,用“Parallel.For”和“Task”对象运行方法“1,000,000,000(十亿)次。
我测量了处理器时间,发现并行效率更高。 Parallel.For 将您的任务划分为小工作项,并以最佳方式在所有内核上并行执行它们。在创建大量任务对象时(仅供参考,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 是一个非常糟糕的做法。 @Georgi-it 请多多谈论坏事。 对不起,我的错误,我应该澄清一下。我的意思是循环创建任务到 1000000000。开销是不可想象的。更何况 Parallel 一次不能创建超过 63 个任务,这使得它在这种情况下更加优化。 这适用于 1000000000 个任务。但是,当我处理图像(重复,缩放分形)并执行 Parallel.For 在线时,许多内核在等待最后一个线程完成时处于空闲状态。为了让它更快,我自己将数据细分为 64 个工作包并为其创建任务。 (然后 Task.WaitAll 等待完成。)这个想法是让空闲线程拿起一个工作包来帮助完成工作,而不是等待 1-2 个线程完成他们(Parallel.For)分配的块。跨度>Mehthod1()
在这个例子中做了什么?【参考方案3】:
Parallel.ForEach 将优化(甚至可能不启动新线程)并阻塞,直到循环完成,并且 Task.Factory 将为每个项目显式创建一个新任务实例,并在它们完成之前返回(异步任务)。 Parallel.Foreach 效率更高。
【讨论】:
【参考方案4】:在我看来,最现实的情况是任务需要完成繁重的操作。 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 是老式的方式。对于不统一的工作单元,建议使用 Task。微软 MVP 和 TPL 的设计者还提到,使用任务将更有效地使用线程,即在等待其他单元完成时不会阻塞那么多。以上是关于Parallel.ForEach 与 Task.Factory.StartNew的主要内容,如果未能解决你的问题,请参考以下文章
Parallel.ForEach 与 Task.Factory.StartNew
Task.StartNew() 与 Parallel.ForEach :多个 Web 请求场景
在 .NET 3.5 中将 Parallel.Foreach 与分区器一起使用
Asp.Net 中有没有办法与运行 Parallel.Foreach 的后台线程进行通信