Task.StartNew() 与 Parallel.ForEach :多个 Web 请求场景

Posted

技术标签:

【中文标题】Task.StartNew() 与 Parallel.ForEach :多个 Web 请求场景【英文标题】:Task.StartNew() vs Parallel.ForEach : Multiple Web Requests Scenario 【发布时间】:2015-08-19 20:28:58 【问题描述】:

我已经阅读了 SO 中的所有相关问题,但对于触发多个 Web 服务调用的场景的最佳方法有点困惑。

我有一个聚合器服务,它接受输入,解析并将其转换为多个 Web 请求,进行 Web 请求调用(不相关,因此可以并行触发)并合并发送回调用者的响应。现在使用下面的代码-

list.ForEach((object obj) =>

     tasks.Add(Task.Factory.StartNew((object state) => 
     
           this.ProcessRequest(obj);
     , obj, CancellationToken.None,
     TaskCreationOptions.AttachedToParent, TaskScheduler.Default));
);
await Task.WhenAll(tasks);

await Task.WhenAll(tasks) 来自 Scott Hanselman 的 post,据说

“斯蒂芬说,从可扩展性的角度来看,一个更好的解决方案是 利用异步 I/O。当你呼唤对面 网络,没有理由(除了方便)阻止 线程等待响应返回”

现有代码似乎消耗了太多线程,并且处理器时间在生产负载上飙升至 100%,这让我开始思考。

另一种替代方法是使用 Parallel.ForEach,它使用分区器但也“阻止”调用,这对我的场景来说很好。

考虑到这是所有“异步 IO”工作而不是“CPU 绑定”工作,并且 Web 请求运行时间不长(最多 3 秒返回),我倾向于相信现有代码已经足够好。但这会提供比 Parallel.ForEach 更好的吞吐量吗? Parallel.ForEach 可能使用“最少”数量的任务,因为分区和线程的最佳使用(?)。我确实用一些本地测试测试了 Parallel.ForEach,但它似乎并没有更好。

目标是减少 CPU 时间并提高吞吐量,从而提高可扩展性。是否有更好的方法来并行处理 Web 请求?

感谢任何输入,谢谢。

编辑: 代码示例中显示的 ProcessRequest 方法确实使用 HttpClient 及其异步方法来触发请求(PostAsync、GetAsync、PutAsync)。

【问题讨论】:

如果ProcessRequest 使用异步方法,为什么要在Task.Factory.StartNew 内部调用它?您可以简单地将它返回的任务添加到您的列表中。如果您实际上在其中阻塞,则在其中的某些部分使用异步方法并不重要。最后的阻塞调用否定了任何好处 “除了方便”很好,这是一个很好的理由。 【参考方案1】:

进行网络请求调用(不相关,因此可以并行触发)

您真正想要的是同时调用它们,而不是并行。即“同时”,而不是“使用多个线程”。

现有代码似乎消耗了太多线程

是的,我也这么认为。 :)

考虑到这是所有“异步 IO”工作,而不是“CPU 绑定”工作

那么这一切都应该异步完成,并且使用任务并行或其他并行代码。

正如 Antii 指出的,你应该让你的异步代码异步:

public async Task ProcessRequestAsync(...);

那么你想要做的是使用 异步并发 (Task.WhenAll),而不是并行并发 (StartNew/Run/Parallel ):

await Task.WhenAll(list.Select(x => ProcessRequestAsync(x)));

【讨论】:

并行和并发是同义词。在此答案中使用“并行”时,您的意思似乎是“多线程”。 Then it should all be done asynchronously, and not using TPL or parallel code. 不应使用 TPL 的 StartNewRun;使用 TPL 来管理代表异步工作的任务会很好,因为这实际上就是您所展示的。你不是“不使用 TPL”,你只是用不同的方式.. 不同意“并行”和“并发”术语。但是您对 TPL 的看法是正确的;我的意思是说“任务​​并行”。 并行做事就是同时做多件事。您可以通过使用多个线程,或通过同时执行多个固有的异步操作来同时执行多项操作。这两种操作都会产生并行性。 .NET 中的Parallel 的操作都涉及多线程,而不是任何其他实现并行的方式,但“并行”或“并行执行”的一般概念绝不是特定于多个线程。是什么让你认为它会是? 对于普通英语,我同意它们是同义词。但是对于开发人员来说,区分并发、并行和异步是有益的。我总是使用并发作为“父”概念,用并行和异步来描述具体的方法。否则,IMO 的术语会令人困惑。 但是,无论是在更广泛的英语上下文中,还是在编程上下文中,并行性这个术语都没有任何东西使它特定于多线程的使用。并行性可以通过使用异步或通过使用多线程来实现。我同意这里有很多相似但略有不同的术语,很难保持直截了当。我只是说定义更多的是您将并行/并发作为同义词(即使在编程上下文中),并且可以通过多线程或异步来实现并行。【参考方案2】:

如果您受 CPU 限制(您是 - “处理器时间高达 100%”),您需要减少 CPU 使用率。异步 IO 对此没有任何帮助。如果有的话,它会导致更多的 CPU 使用(此处不明显)。

分析应用程序以查看占用如此多 CPU 时间的原因并优化该代码。

您启动并行的方式(并行、任务、异步 IO)对并行操作本身的效率没有任何影响。如果您以异步方式调用它,网络不会变得更快。它仍然是相同的硬件。也不会减少 CPU 使用率。

通过实验确定最佳并行度,并选择适合该度的并行技术。如果是几十个,那么线程就完全没问题了。如果是数百个,请认真考虑异步 IO。

【讨论】:

【参考方案3】:

在 Task.Factory.StartNew 中包装同步调用不会给您带来任何异步的好处。您应该使用适当的异步函数以获得更好的可伸缩性。请注意 Scott Hanselman 在您所指的帖子中如何使用异步函数。

例如

public async Task<bool> ValidateUrlAsync(string url)

    using(var response = (HttpWebResponse)await WebRequest.Create(url).GetResponseAsync())
    return response.StatusCode == HttpStatusCode.Ok;

结帐http://blogs.msdn.com/b/pfxteam/archive/2012/03/24/10287244.aspx

所以, 你的 ProcessRequest 方法应该像异步一样实现

public async Task<bool> ProcessRequestAsync(...)

那么你就可以了

tasks.Add(this.ProcessRequestAsync(obj))

如果您使用 Task.Factory.StartNew 启动任务,即使您的 ProcessRequest 方法在内部进行异步调用,它也不能作为异步工作。如果你想使用 Task.Factory 你应该让你的 lambda 也异步:

tasks.Add(Task.Factory.StartNew(async (object state) => 

    await this.ProcessRequestAsync(obj);
, obj, CancellationToken.None, TaskCreationOptions.AttachedToParent,   TaskScheduler.Default));

【讨论】:

可能我错过了提及...实际上,ProcessRequest 反过来会根据传入的请求 (obj) 调用异步版本的 HttpClient API - PostAsync、SendAsync 和 GetAsync。将更新问题. 只将异步函数添加到您的任务列表中。不要使用 Task.Factory.StartNew。 他受 CPU 限制。异步 IO 不会提供更多吞吐量。 好吧,他说“考虑到这是所有“异步 IO”工作,而不是“CPU 绑定”工作”,并表示他正在使用 HttpClient 进行异步 Web 请求。这个 CPU 是如何绑定的? CPU 受限于他将 CPU 驱动到 100% 的事实。这限制了他获得的吞吐量。

以上是关于Task.StartNew() 与 Parallel.ForEach :多个 Web 请求场景的主要内容,如果未能解决你的问题,请参考以下文章

series和 paralle

GETTING STARTED WITH DISTRIBUTED DATA PARALLE

数据泵expdp 在rac环境下 paralle 的处理方法

并发与并行

ParNew收集器

详解c++11多线程