Task.Factory.StartNew 或 Parallel.ForEach 用于许多长时间运行的任务

本文关键字:长时间 许多 运行 任务 用于 ForEach Factory StartNew Parallel Task | 更新日期: 2023-09-27 18:34:54

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

我需要每晚在

ThreadPool中运行大约 1,000 个任务(将来这个数字可能会增加(。每个任务都在执行长时间运行的操作(从 Web 服务读取数据(,并且不会占用大量 CPUAsync 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.LongRunningMaxDegreeOfParallelism = Environment.ProcessorCount * SomeConstant用法之间的比较。

Task.Factory.StartNew 或 Parallel.ForEach 用于许多长时间运行的任务

也许你不知道这一点,但Parallel类中的成员只是围绕Task对象的(复杂(包装器。 如果您想知道,Parallel 类使用 TaskCreationOptions.None 创建Task对象。 但是,无论将哪些创建选项传递给任务对象的构造函数,MaxDegreeOfParallelism都会影响这些任务对象。

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

(

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

您可能想要Parallel.ForEach . 但是,将MaxDegreeOfParallelism等于如此高的数字实际上并不能保证同时运行那么多线程,因为任务仍将由ThreadPoolTaskScheduler控制。 该调度程序将一次运行的线程数增加到尽可能小的数量,我认为这是两种方法之间的最大区别。 您可以编写(并指定(自己的TaskScheduler来模拟最大程度的并行行为,并具有两全其美的效果,但我怀疑您是否有兴趣做某件事。

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

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

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

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

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

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

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

我从未听说过异步I/O在某种程度上"不是一种选择"的情况。我什至无法想象此约束的任何有效用例。如果您无法使用异步 I/O,则表明必须尽快修复严重的设计问题。

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

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

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