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

9

我已经阅读了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的文章,其中提到:

"从可扩展性的角度来看,一个更好的解决方案是利用异步I/O。当你在跨网络调用时,没有理由(除了方便)阻塞线程等待响应返回"

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

另一个选择是使用Parallel.ForEach,它使用分区器但也会“阻塞”调用,对于我的情况来说这是可以接受的。

考虑到这都是“异步I/O”工作而不是“CPU绑定”工作,并且Web请求不会长时间运行(最多3秒返回),我倾向于认为现有代码已经足够好了。但是,与Parallel.ForEach相比,这是否能提供更好的吞吐量?Parallel.ForEach可能使用“最小”数量的任务,因为分区,因此可以最优地利用线程(?)。我进行了一些本地测试并没有发现Parallel.ForEach更好。

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

非常感谢任何意见,谢谢。

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


1
如果“ProcessRequest”使用异步方法,为什么要在“Task.Factory.StartNew”内调用它?您可以简单地将其返回的任务添加到列表中。如果您实际上是在其中阻塞,那么使用其中某些部分的异步方法并不重要。最终的阻塞调用抵消了任何好处。 - Panagiotis Kanavos
“除了方便之外”,那是一个非常好的理由。 - usr
3个回答

7

使Web请求调用(无关,因此可以并行触发)

实际上,您想要的是同时调用它们,而不是并行调用。也就是说,“同时进行”,而不是“使用多个线程”。

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

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

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

那么应该全部异步完成,而不是使用任务并行或其他并行代码。

正如Antii所指出的那样,您应该将异步代码变成异步的:

public async Task ProcessRequestAsync(...);

那么您想要做的是使用异步并发(Task.WhenAll)来消耗它,而不是使用并行并发(StartNew/Run/Parallel)。
await Task.WhenAll(list.Select(x => ProcessRequestAsync(x)));

并行和并发是同义词。在这个答案中使用“parallel”时,似乎你的意思是“多线程”。然后应该全部异步完成,而不是使用TPL或并行代码。不应该使用TPL的StartNew或Run;使用TPL来管理表示异步工作的任务是可以的,因为这实际上就是你展示的内容。你并没有“不使用TPL”,你只是以不同的方式使用它。 - Servy
2
不同意“并行”和“并发”术语的使用。但是您关于TPL的说法是正确的;我想说的是“任务并行”。 - Stephen Cleary
3
对于通用英语,我同意它们是同义词。但作为开发人员,区分并发、并行和异步是有益的。我总是使用并发作为“父”概念,使用并行和异步来描述具体的方法。否则,在我看来术语会很混乱。 - Stephen Cleary
但是在更广泛的英语环境中,甚至在编程环境中,"Parallelism"这个术语并没有什么特定于使用多线程的地方。并行性可以通过异步或多线程来实现。我同意这里有很多类似但微妙不同的术语,很难区分它们。我只是想说,定义更多的是将并行性/并发性视为同义词(即使在编程环境中),并且并行性可以通过多线程或异步来实现。 - Servy
我认为尝试向人们解释“异步不能用于实现并行”会导致相当大的困惑。您可以完全使用异步来并行执行工作;您不需要使用多个线程来并行执行工作。如果您使用术语“多线程”而不是“并行”,那么您就有了一个特别描述通过使用多个线程实现并行的术语。 - Servy
显示剩余2条评论

3
如果您的CPU占用过高(您会看到"处理器时间飙升到100%"),那么您需要降低CPU使用率。异步IO对此无济于事,甚至可能导致更多的CPU使用(这里是无法察觉的)。
通过对应用程序进行分析,找出占用大量CPU时间的代码,并优化它。
启动并行操作的方式(Parallel、Task、异步IO)对并行操作本身的效率没有影响。如果您以异步方式调用网络,它也不会变得更快。硬件仍然是相同的,CPU使用率也不会减少。
根据实验确定最佳的并行度,选择适合该度数的并行技术。如果只有几十个,则线程完全可以胜任。如果是上百个,则应认真考虑使用异步IO。

0

将同步调用包装在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。我会更新问题。 - Lalman
他的CPU负载很高。异步IO无法提供更高的吞吐量。 - usr
他说:“考虑到这都是异步IO工作,而不是CPU密集型工作”,并表示他正在使用HttpClient进行异步网络请求。那这怎么会是CPU密集型的呢? - Antti Leppänen
该程序是CPU绑定的,仅因为它将CPU的利用率推向了100%。这限制了他获得的吞吐量。 - usr
阻塞不会消耗CPU(除了一点点恒定的量)。 阻塞会将线程从CPU中取消调度。 不要犯认为Thread.Sleep(100)会烧掉100ms CPU时间的错误! 它只会烧掉大约0.1ms的开销,用于内核调用和上下文切换。 - usr
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接