11

可能重复:
Parallel.ForEach vs Task.Factory.StartNew

我需要ThreadPool每晚运行大约 1,000 个任务(这个数字将来可能会增加)。每个任务都在执行长时间运行的操作(从 Web 服务读取数据)并且不是 CPU 密集型的。Async I/O不是此特定用例的选项。

给定一个IList<string>参数,我需要DoSomething(string x). 我试图在以下两个选项之间进行选择:

IList<Task> tasks = new List<Task>();
foreach (var p in parameters)
{
    tasks.Add(Task.Factory.StartNew(() => DoSomething(p), TaskCreationOptions.LongRunning));
}
Task.WaitAll(tasks.ToArray());

或者

Parallel.ForEach(parameters, new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount*32}, DoSomething);

哪个选项更好,为什么?

笔记 :

答案应包括 和 的用法之间的TaskCreationOptions.LongRunning比较MaxDegreeOfParallelism = Environment.ProcessorCount * SomeConstant

4

3 回答 3

36

也许您没有意识到这一点,但Parallel类中的成员只是对象周围的简单(复杂)包装器Task。如果您想知道,Parallel该类Task使用TaskCreationOptions.None. 但是,MaxDegreeOfParallelism无论将什么创建选项传递给任务对象的构造函数,都会影响这些任务对象。

TaskCreationOptions.LongRunning给底层一个“提示” TaskScheduler,它可能在线程超额订阅时表现更好。超额订阅适用于高延迟的线程,例如 I/O,因为它会将多个线程(是线程,不是任务)分配给单个内核,以便它始终有事可做,而不是等待当线程处于等待状态时完成的操作。在TaskScheduler使用 的情况下ThreadPool,它将自己的专用线程上运行 LongRunning 任务(每个任务都有一个线程的唯一情况),否则它将正常运行,调度和工作窃取(真的,无论如何你想要什么)

MaxDegreeOfParallelism控制运行的并发操作数。它类似于指定数据将被拆分和处理的最大分区数。如果TaskCreationOptions.LongRunning能够被指定,所有这一切都会限制一次运行的任务数量,类似于将TaskScheduler最大并发级别设置为该值的 a,类似于此示例

你可能想要Parallel.ForEach. 但是,添加MaxDegreeOfParallelism等于这么大的数字实际上并不能保证同时运行那么多线程,因为任务仍然由ThreadPoolTaskScheduler. 该调度程序将一次运行的线程数尽可能减少,我认为这是两种方法之间的最大区别。您可以编写(并指定)您自己的TaskScheduler模仿最大程度的并行行为,并拥有两全其美,但我怀疑您有兴趣做的事情。

我的猜测是,根据延迟和您需要执行的实际请求的数量,使用任务在许多(?)情况下会表现得更好,尽管最终会使用更多的内存,而并行在资源使用方面会更加一致。当然,异步 I/O 的性能将比这两个选项中的任何一个都好,但我知道你不能这样做,因为你使用的是遗留库。所以,不幸的是,无论你选择哪一个,你都会陷入平庸的表现。

一个真正的解决方案是想办法让异步 I/O 发生。由于我不了解情况,因此我认为没有比这更有帮助的了。您的程序(读取、线程)将继续执行,内核将等待 I/O 操作完成(这也称为使用 I/O 完成端口)。因为线程不处于等待状态,所以运行时可以在更少的线程上做更多的工作,这通常最终会在内核数和线程数之间达到最佳关系。正如我所希望的那样,添加更多线程并不等同于更好的性能(实际上,由于上​​下文切换之类的原因,它通常会损害性能)。

但是,整个答案对于确定您的问题的最终答案是没有用的,尽管我希望它会给您一些需要的方向。在您对其进行分析之前,您不会知道什么表现更好。如果您不同时尝试它们(我应该澄清我的意思是没有 LongRunning 选项的任务,让调度程序处理线程切换)并分析它们以确定最适合您的特定用例的方法,那么您就是在卖空自己。

于 2012-05-21T18:02:40.990 回答
4

这两个选项都完全不适合您的方案。

TaskCreationOptions.LongRunning对于不受 CPU 限制的任务来说,这无疑是一个更好的选择,因为 TPL(Parallel类/扩展)几乎专门用于通过在多个内核(而不是线程)上运行受 CPU 限制的操作来最大化其吞吐量。

然而,1000 个任务是一个不可接受的数字。它们是否同时运行并不是问题所在。即使 100 个线程在同步 I/O 上等待也是站不住脚的。正如其中一条评论所暗示的那样,您的应用程序将使用大量内存并最终将几乎所有时间都花在上下文切换上。TPL 不是为这种规模设计的。

如果您的操作受 I/O 限制——并且如果您正在使用 Web 服务,那么它们就是——那么异步 I/O 不仅是正确的解决方案,而且是唯一的解决方案。如果您必须重新设计您的某些代码(例如,将异步方法添加到原来没有的主要接口),请执行此操作,因为 I/O 完成端口是Windows 或 .NET 中唯一的机制可以适当地支持这种特定类型的并发。

我从未听说过异步 I/O 以某种方式“不是一种选择”的情况。我什至无法为这个约束设想任何有效的用例。如果您无法使用异步 I/O,那么这表明存在必须尽快修复的严重设计问题。

于 2012-05-21T18:13:32.593 回答
4

虽然这不是直接比较,但我认为它可能会对您有所帮助。我做了类似于你描述的事情(在我的情况下,我知道在另一端有一个负载平衡的服务器集群服务于 REST 调用)。如果我还使用以下代码告诉我的操作系统它可以连接到比通常数量更多的端点,我可以使用Parrallel.ForEach启动最佳数量的工作线程来获得良好的结果。

    var servicePointManager = System.Net.ServicePointManager.FindServicePoint(Uri);
    servicePointManager.ConnectionLimit = 250;

请注意,您必须为连接到的每个唯一 URL 调用一次。

于 2012-05-21T19:17:11.030 回答