使用Parallel.foreach时,无法获得正确的http查询并行度
本文关键字:http 并行度 查询 foreach Parallel 使用 | 更新日期: 2023-09-27 18:29:57
我正在尝试同时执行2000个http查询。使用此代码进行的测试(主要由响应服务器组成)在大约15秒内运行:
public void testTasks()
{
var urls = new List<string>();
urls.AddRange(createUrls());
var start = DateTime.Now;
ConcurrentQueue<string> contents = new ConcurrentQueue<string>();
Task.WaitAll(urls.Select(url =>
{
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
return client.GetAsync(url).ContinueWith(response =>
{
try
{
var content = response.Result.Content.ReadAsStringAsync().Result;
contents.Enqueue(content);
}
catch (Exception e)
{
}
});
}).ToArray());
var end = DateTime.Now;
var time = end - start;
Console.WriteLine("Time spent in Tasks : " + time.TotalSeconds);
Console.WriteLine("Queue size : " + contents.Count);
}
现在我用Parallel.foreach进行同样的测试,我得到了1分18秒的运行时间:
public void testParallelForeach()
{
var urls = new List<string>();
urls.AddRange(createUrls());
var start = DateTime.Now;
ConcurrentQueue<string> contents = new ConcurrentQueue<string>();
Parallel.ForEach(urls, new ParallelOptions() { MaxDegreeOfParallelism = urls.Count }, url =>
{
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
try
{
string content = client.GetStringAsync(url).Result;
contents.Enqueue(content);
}
catch (Exception e)
{
}
});
var end = DateTime.Now;
var time = end - start;
Console.WriteLine("Time spent in ParallelForeach : " + time.TotalSeconds);
Console.WriteLine("Queue size : " + contents.Count);
}
正如你所看到的,我使用的MaxDegreeOfParallelism等于服务器的数量。但这似乎还不够。
编辑
所以我的问题是:
-为什么我的表现会有这样的差异?
-我们能用并行的foreach实现同样的性能吗?
我想提供一个与Scott Chamberlain提供的角度略有不同的角度。使用同步还是异步IO并不重要。重要的是使用了什么有效的DOP(并行度)来发送HTTP请求。
不同的web服务会喜欢不同的DOP,您需要根据经验确定最佳DOP。
CPU内核的数量与最佳IO DOP无关。只是要确保这一点被理解。
第一个解决方案很糟糕,因为它选择了DOP=urls。计数第二个是坏的,因为它选择DOP作为TPL决定的任何值(它是而不是等于MaxDegreeOfParallelism = urls.Count
,因为这是的最大值)。这些值中没有一个可能是最优的。
- 使用一种众所周知的技术来实现具有精确DOP的并行异步foreach(例如。http://blogs.msdn.com/b/pfxteam/archive/2012/03/05/10278165.aspx)
- 通过实验确定最佳DOP
这是一个很容易解决的问题。你陷入了使用TPL内置设施来解决问题的常见陷阱。TPL缺乏适当解决这一问题所需的工具。
还要注意,调用Result
否定了异步IO的好处(这在这里非常有用)。请改用await
。
-为什么我的表现会有这样的差异?
因为使用Parallel.ForEach
进行IO绑定工作毫无意义。Parallel.ForEach
仅用于CPU绑定工作。此外,Parallel.ForEach
具有"渐变"效果,它不是从MaxDegreeOfParallelism
开始的,它从1个线程开始,然后在检测到工作可能使用更多线程时不断添加线程,直到达到MaxDegreeOfParallelism
(这是有原因的,它被称为"MaxDegreeOfParallelism"而不是"DegreeOfParallelism")。使用IO绑定的工作而不是CPU绑定的工作会极大地扰乱调度算法。
-我们能用并行的foreach实现同样的性能吗?
不,你不能,因为你在工作中使用了错误的工具,不正确的方法总是优于更正确的方法(你的第一种方法是)
然而,使用Async
方法然后立即调用它的.Result
仍然是一种错误的方法,最好的方法是使用适当的异步/等待(也可以使用Stopwatch
而不是DateTime
来测量经过的时间并处理您创建的一次性类)
public async Task TestTasksProperly()
{
var urls = new List<string>();
urls.AddRange(createUrls());
var stopwatch = Stopwatch.StartNew();
ConcurrentQueue<string> contents = new ConcurrentQueue<string>();
await Task.WhenAll(urls.Select(url => QueryUrl(url, contents)))).ConfigureAwait(false);
var time = stopwatch.ElapsedMilliseconds / 1000.0;
Console.WriteLine("Time spent in Tasks : " + time);
Console.WriteLine("Queue size : " + contents.Count);
}
private static async Task QueryUrl(string url, ConcurrentQueue<string> contents)
{
using (var client = new HttpClient {Timeout = TimeSpan.FromSeconds(10)})
{
using (var response = await client.GetAsync(url).ConfigureAwait(false))
{
try
{
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
contents.Enqueue(content);
}
catch (Exception e)
{
}
}
}
}