使用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实现同样的性能吗?

使用Parallel.foreach时,无法获得正确的http查询并行度

我想提供一个与Scott Chamberlain提供的角度略有不同的角度。使用同步还是异步IO并不重要。重要的是使用了什么有效的DOP(并行度)来发送HTTP请求。

不同的web服务会喜欢不同的DOP,您需要根据经验确定最佳DOP。

CPU内核的数量与最佳IO DOP无关。只是要确保这一点被理解。

第一个解决方案很糟糕,因为它选择了DOP=urls。计数第二个是坏的,因为它选择DOP作为TPL决定的任何值(它是而不是等于MaxDegreeOfParallelism = urls.Count,因为这是的最大值)。这些值中没有一个可能是最优的。

  1. 使用一种众所周知的技术来实现具有精确DOP的并行异步foreach(例如。http://blogs.msdn.com/b/pfxteam/archive/2012/03/05/10278165.aspx)
  2. 通过实验确定最佳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)
            {
            }
        }
    }
}