由于 HttpClient 请求缓慢,Task.Result 在 Parallel.ForEach 内阻塞

Posted

技术标签:

【中文标题】由于 HttpClient 请求缓慢,Task.Result 在 Parallel.ForEach 内阻塞【英文标题】:Task.Result blocking inside Parallel.ForEach due to slow HttpClient request 【发布时间】:2019-07-02 22:15:25 【问题描述】:

我了解将异步 lambda 与 Parallel.ForEach 一起使用的含义,这就是我在这里不使用它的原因。然后,这迫使我对发出 Http 请求的每个任务使用.Result。但是,通过性能分析器运行这个简单的爬虫显示 .Result 的已用独占时间百分比约为 98%,这显然是由于调用的阻塞性质。

我的问题是:是否有可能对其进行优化以使其仍然是异步的?我不确定在这种情况下是否会有所帮助,因为检索 html/XML 可能需要很长时间。

我正在运行一个具有 8 个逻辑核心的 4 核处理器(因此是 MaxDegreesOfParallelism = 8。现在我正在寻找大约 2.5 小时来下载和解析大约 51,000 个简单财务数据的 HTML/XML 页面。

我倾向于使用 XmlReader 而不是 Linq2XML 来加快解析速度,但似乎瓶颈在于 .Result 调用。

虽然在这里应该无关紧要,但 SEC 将抓取限制为 10 个请求/秒。

public class SECScraper

    public event EventHandler<ProgressChangedEventArgs> ProgressChangedEvent;

    public SECScraper(HttpClient client, FinanceContext financeContext)
    
        _client = client;
        _financeContext = financeContext;
    

    public void Download()
    
        _numDownloaded = 0;
        _interval = _financeContext.Companies.Count() / 100;

        Parallel.ForEach(_financeContext.Companies, new ParallelOptions MaxDegreeOfParallelism = 8,
            company =>
            
                RetrieveSECData(company.CIK);
            );
    

    protected virtual void OnProgressChanged(ProgressChangedEventArgs e)
    
        ProgressChangedEvent?.Invoke(this, e);
    

    private void RetrieveSECData(int cik)
    
        // move this url elsewhere
        var url = "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=" + cik +
                  "&type=10-q&dateb=&owner=include&count=100";

        var srBody = ReadHTML(url).Result; // consider moving this to srPage
        var srPage = new SearchResultsPage(srBody);

        var reportLinks = srPage.GetAllReportLinks();

        foreach (var link in reportLinks)
        
            url = SEC_HOSTNAME + link;

            var fdBody = ReadHTML(url).Result;
            var fdPage = new FilingDetailsPage(fdBody);

            var xbrlLink = fdPage.GetInstanceDocumentLink();

            var xbrlBody = ReadHTML(SEC_HOSTNAME + xbrlLink).Result;
            var xbrlDoc = new XBRLDocument(xbrlBody);
            var epsData = xbrlDoc.GetAllEPSData();

            //foreach (var eps in epsData)
            //    Console.WriteLine($"eps.StartDate to eps.EndDate -- eps.EPS");
        

        IncrementNumDownloadedAndNotify();
    

    private async Task<string> ReadHTML(string url)
    
        using var response = await _client.GetAsync(url);
        return await response.Content.ReadAsStringAsync();
    

【问题讨论】:

如果远程站点速率将您限制为 10 个请求/秒,那么您可以检索 51k 个请求的最短理论时间是 85 分钟,与您的 125 分钟标记相差不远。我想说,鉴于许多请求的开销将弥补 10-20 分钟的差异,因此我认为除了尝试删除/增加限制或为每个请求下载更多数据之外,您无能为力。 a) 没有必要使用Parallel.ForEach docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… b) 你可能想要增加DefaultConnectionLimit ***.com/questions/48785681/… c) 一旦b) 完成,使用Semaphore限制并发下载。 Here is an answer 到一个关于限制并发异步 I/O 操作数量的类似问题,它使用 Task.WhenAll 的自定义实现。 【参考方案1】:

该任务不受 CPU 限制,而是受网络限制,因此无需使用多个线程。

在一个线程上进行多个异步调用。 不要等待他们。将任务列在清单上。当您获得一定数量时(假设您希望一次完成 10 个),开始等待第一个完成(查找“任务,WhenAny”以获取更多信息)。

然后添加更多内容 :-) 然后您可以使用其他代码通过 #/秒来控制任务灯的大小。

【讨论】:

@keelerjr12 只是为了澄清为什么你可以在同一个线程上异步运行它,HttpClient 有自己的连接池,因此它可以一次运行多个连接。 @FastAl,我知道它是 IO 绑定的。但是,在我必须使用 HTML Agility Pack 和 Linq2XML 解析 HTML/XML 的情况下,多线程是否无济于事?有了 8 个逻辑核心,我似乎可以利用它们来分别解析 Http 请求后返回的不同文档。因此,有点受 CPU 限制。 @keelerjr12,是的,这可能会减慢速度。查看您可以使用的通用阻塞集合 或并行队列 - 您将在一个线程上下载具有上述多个异步的所有内容,并将其放入队列中以供其他线程处理。但实际上,下载可能仍然需要更长的时间并且您不会获得任何收益,因为这是您的线程正在做的唯一真正的工作。一个简单的发现方法是 - 查看您正在使用的一个 CPU 是否与监控挂钩。 (可能需要设置亲和力?)【参考方案2】:

是否有可能对其进行优化以使其仍然是异步的?

是的。我不确定你为什么首先使用Parallel;对于此类问题,这似乎是错误的解决方案。您需要跨项目集合执行异步工作,因此更适合的是异步并发;这是使用Task.WhenAll

public class SECScraper

  public async Task DownloadAsync()
  
    _numDownloaded = 0;
    _interval = _financeContext.Companies.Count() / 100;

    var tasks = _financeContext.Companies.Select(company => RetrieveSECDataAsync(company.CIK)).ToList();
    await Task.WhenAll(tasks);
  

  private async Task RetrieveSECDataAsync(int cik)
  
    var url = "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=" + cik +
        "&type=10-q&dateb=&owner=include&count=100";

    var srBody = await ReadHTMLAsync(url);
    var srPage = new SearchResultsPage(srBody);

    var reportLinks = srPage.GetAllReportLinks();

    foreach (var link in reportLinks)
    
      url = SEC_HOSTNAME + link;

      var fdBody = await ReadHTMLAsync(url);
      var fdPage = new FilingDetailsPage(fdBody);

      var xbrlLink = fdPage.GetInstanceDocumentLink();

      var xbrlBody = await ReadHTMLAsync(SEC_HOSTNAME + xbrlLink);
      var xbrlDoc = new XBRLDocument(xbrlBody);
      var epsData = xbrlDoc.GetAllEPSData();
    

    IncrementNumDownloadedAndNotify();
  

  private async Task<string> ReadHTMLAsync(string url)
  
    using var response = await _client.GetAsync(url);
    return await response.Content.ReadAsStringAsync();
  

另外,我建议您使用IProgress&lt;T&gt; 报告进度。

【讨论】:

我实际上使用了与您最初提议的解决方案类似的解决方案,但没有限制 HTTP 请求的速率,因此我被禁止了。我将使用您提出的解决方案,然后装饰 HttpClient 以将我的请求限制为 10 个请求/秒。 在我的 Getxxx() 方法中,我实际上是在解析 html 和 xml,在这种情况下多线程没有帮助吗?对我来说,它似乎也受 CPU 限制。 @keelerjr12:CPU 密集型,是的,但它是 CPU 密集型的吗?以我的经验,解析 html 非常快。了解并行性是否有帮助的唯一方法是双向测量。如果您确实想探索添加并行性,我会推荐 TPL Dataflow 或使用 Channels with Parallel;这样一来,您的异步工作就与 CPU 密集型工作分开了。

以上是关于由于 HttpClient 请求缓慢,Task.Result 在 Parallel.ForEach 内阻塞的主要内容,如果未能解决你的问题,请参考以下文章

由于 CORS 问题,无法在 Angular 中发出 HttpClient 发布请求

区分 HttpClient 请求失败类型

HttpClient的基本使用

HttpClient学习--HttpClient的POST请求过程源码解读

HttpClient和AsynchttpClient的get与post请求方式

JAVA利用HttpClient进行POST请求(HTTPS)