parallel.foreach和httpclient - 奇怪的行为

时间:2016-07-06 10:37:42

标签: c# parallel-processing asp.net-web-api2

我有一段代码循环遍历集合并为每次迭代调用httpclient。 httpclient调用的api平均需要30-40ms才能执行。顺序调用它,我得到预期的结果,但是一旦我使用Parallel.foreach,它需要更长的时间。仔细查看日志,我可以看到很多httpclient调用需要1000ms才能执行,然后时间会回落到30-40ms。查看api日志,我可以看到它几乎没有超过100毫秒。我不知道为什么会出现这种情况。

代码是

using (var client = new HttpClient())
{
  var content = new StringContent(parameters, Encoding.UTF8, "application/json");
  var response = client.PostAsync(url, content);
  _log.Info(string.Format("Took {0} ms to send post", watch.ElapsedMilliseconds));
  watch.Restart();

  var responseString = response.Result.Content.ReadAsStringAsync();
  _log.Info(string.Format("Took {0} ms to readstring after post", watch.ElapsedMilliseconds));
}

并行调用是这样的

    Console.WriteLine("starting parallel...");
    Parallel.ForEach(recipientCollections, recipientCollection => 
      {    
        // A lot of processing happens here to create relevant content
        var secondaryCountryRecipientList = string.Join(",",refinedCountryRecipients);
        var emailApiParams = new SendEmailParametersModel(CountrySubscriberApplicationId,
                                        queueItem.SitecoreId, queueItem.Version, queueItem.Language, countryFeedItem.Subject,
                                        countryFeedItem.Html, countryFeedItem.From, _recipientsFormatter.Format(secondaryCountryRecipientList));

       log.Info(string.Format("Sending email request for {0}. Recipients {1}",                                        queueItem.SitecoreId, secondaryCountryRecipientList));

        var response = _notificationsApi.Invoke(emailApiParams);
        });

感谢

2 个答案:

答案 0 :(得分:5)

默认情况下,.NET每个服务器只允许2个连接。要更改此设置,您必须将ServicePointManager.DefaultConnectionLimit的值更改为更大的值,例如20或100.

如果您发出太多请求,这不会阻止服务器泛滥或消耗太多内存。一个更好的选择是使用ActionBlock< T>缓冲请求并在受控函数中并行发送它们,例如:

 ServicePointManager.DefaultConnectionLimit =20;

 var client = new HttpClient();

 var blockOptions=new ExecutionDataflowBlockOptions{MaxDegreeOfParallelism=10};

 var emailBlock=new ActionBlock<SendEmailParametersModel>(async arameters=>
     {
         var watch=new Stopwatch();
         var content = new StringContent(parameters, Encoding.UTF8, "application/json");
         var response = await client.PostAsync(url, content);
         _log.Info(..);
         watch.Restart();

         var responseString = await response.Result.Content.ReadAsStringAsync();
         _log.Info(...);
 });

发送电子邮件不再需要并行调用:

foreach(var recipientCollection in recipientCollections)
{
    var secondaryCountryRecipientList = string.Join(",",refinedCountryRecipients);
    var emailApiParams = new SendEmailParametersModel(CountrySubscriberApplicationId, queueItem.SitecoreId, queueItem.Version, queueItem.Language, countryFeedItem.Subject,countryFeedItem.Html, countryFeedItem.From, _recipientsFormatter.Format(secondaryCountryRecipientList));

   emailBlock.Post(emailApiParams);
   log.Info(...);
}
emailBlock.Complete();
await emailBlock.Completion();

HttpClient是线程安全的,允许您对所有请求使用相同的客户端。

上面的代码将缓冲所有请求并一次执行10个请求。调用Complete()告诉块完成所有操作并停止处理新消息。 await emailBlock.Completion()等待所有现有消息完成后再继续

答案 1 :(得分:2)

您正在超载服务器。 Parallel不知道有多少线程最适合您的特定Web服务。你会得到不稳定的结果。事实上,如果循环运行很长一段时间,线程数可以上升到数百甚至数千(真的!)。根据经验确定正确的DOP并修复DOP。

当服务超载时,看到非常高的服务时间并不罕见。怎么可能呢?没有足够的能力快速完成。

 var responseString = response.Result.Content.ReadAsStringAsync()

在这里,您错过了.Result来电。目前的时机已经结束,但这并没有改变结论。

您也可能遇到HTTP调用的.NET并发请求限制。默认值为2.