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 的 StartNew
或 Run
;使用 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 请求场景的主要内容,如果未能解决你的问题,请参考以下文章
GETTING STARTED WITH DISTRIBUTED DATA PARALLE