HttpClient - 发送一批请求

11
我想迭代一批请求,使用HttpClient类将每个请求发送到外部API。
  foreach (var MyRequest in RequestsBatch)
  {
            try
            {
                HttpClient httpClient = new HttpClient();
                httpClient.Timeout = TimeSpan.FromMilliseconds(5);
                HttpResponseMessage response = await httpClient.PostAsJsonAsync<string>(string.Format("{0}api/GetResponse", endpoint), myRequest);
                JObject resultResponse = await response.Content.ReadAsAsync<JObject>();
            }
            catch (Exception ex)
            {
                continue;
            }
 }

这里的背景是我需要设置一个非常小的超时时间,以便在响应时间超过该时间后,我们将简单地获得“任务已取消”异常并继续迭代。

现在,在上面的代码中,请注释掉这两行:

                HttpResponseMessage response = await httpClient.PostAsJsonAsync<string>(string.Format("{0}api/GetResponse", endpoint), myRequest);
                resultResponse = await response.Content.ReadAsAsync<JObject>();

循环非常快。取消注释并再次尝试。这需要很长时间。

我想知道使用await调用PostAsJsonAsync/ReadAsAsync方法是否比超时值需要更长的时间?

根据下面的答案,假设它将创建不同的线程,我们有这个方法:

  public Task<JObject> GetResponse(string endPoint, JObject request, TimeSpan timeout)
    {
        return Task.Run(async () =>
        {
            try
            {
                HttpClient httpClient = new HttpClient();
                httpClient.Timeout = TimeSpan.FromMilliseconds(5);
                HttpResponseMessage response = await httpClient.PostAsJsonAsync<string>(string.Format("{0}api/GetResponse", endPoint), request).WithTimeout<HttpResponseMessage>(timeout);
                JObject resultResponse = await response.Content.ReadAsAsync<JObject>().WithTimeout<JObject>(timeout);
                return resultResponse;
            }
            catch (Exception ex)
            {
                return new JObject() { new JProperty("ControlledException", "Invalid response. ")};
            }
        });
    }

在那里会引发一个异常,应返回JObject异常,但是如果使用httpClient方法,即使它引发异常,也需要很长时间。是否有在幕后处理影响任务的处理方式,即使返回值只是简单的异常JObject?

如果是,还有哪些其他方法可以以非常快的方式发送一批请求到API?


你正在进行多少个并发请求?听起来瓶颈出现在等待线程池线程开始运行时。 - RagtimeWilly
@RagtimeWilly 大约200-300个请求。 - Alberto Montellano
现在是尝试使用“Parallel.ForEach”的好时机。 - EZI
@EZI 在这种情况下,Parallel.ForEach 不是最合适的选择。只需正确使用 async await 将请求分批发送即可。 - Jerry Joseph
2
尝试重复使用HttpClient而不是为每个请求实例化新的HttpClient。HttpClient旨在进行并发请求。请阅读https://blogs.msdn.microsoft.com/henrikn/2012/02/16/httpclient-is-here/中的注释。 - Jerry Joseph
2个回答

38

我同意接受的答案,即加快速度的关键是并行运行请求。但是,通过使用Task.RunParallel.ForEach强制将其他线程添加到混合中的任何解决方案都不会在I/O绑定异步操作方面提高效率。如果有什么作用,那就是有害的。

您可以轻松地让所有调用同时运行,同时让底层异步子系统决定需要多少线程才能尽可能有效地完成任务。很有可能这个数字要比并发调用的数量小得多,因为它们在等待响应时根本不需要任何线程。

此外,接受的答案为每个调用创建了一个新的HttpClient实例。也不要这样做-坏事情会发生

以下是接受的答案的修改版本:

var httpClient = new HttpClient {
    Timeout = TimeSpan.FromMilliseconds(5)
};

var taskList = new List<Task<JObject>>();

foreach (var myRequest in RequestsBatch)
{
    // by virtue of not awaiting each call, you've already acheived parallelism
    taskList.Add(GetResponseAsync(endPoint, myRequest));
}

try
{
    // asynchronously wait until all tasks are complete
    await Task.WhenAll(taskList.ToArray());
}
catch (Exception ex)
{
}

async Task<JObject> GetResponseAsync(string endPoint, string myRequest)
{
    // no Task.Run here!
    var response = await httpClient.PostAsJsonAsync<string>(
        string.Format("{0}api/GetResponse", endpoint), 
        myRequest);
    return await response.Content.ReadAsAsync<JObject>();
}

3
回答时不确定当时的建议是什么,但截至今天的建议是不要创建太多新的HttpClient,而是尽可能重用同一个。请注意不要改变原意。 - superjos
很酷。仅供记录,我最近读到这个故事的另一个变化:随着即将推出的netcore 2.1,您可以在技术上创建任意数量的HttpClient,它们并不真正昂贵。工厂负责控制内部HttpClientHandler的创建,这是实际昂贵的组件。这里有一篇关于此的文章 - superjos
我对任务和async/await进行了一些测试,发现它们实际上只是将工作排队到线程池中。它们总是会将工作放在后台线程上。该线程在等待I/O操作返回时会挂起。挂起的线程仍然计入线程池的最大线程数。尽管如此,这是一个很好的解决方案,因为您不需要创建新线程的开销,而是在线程池上运行。这种解决方案的缺点是,由于线程池是静态类,所以您受制于线程池的配置。 - Chris Rollins

2

看起来你并没有为每个请求都运行一个单独的线程。尝试像这样做:

var taskList = new List<Task<JObject>>();

foreach (var myRequest in RequestsBatch)
{
    taskList.Add(GetResponse(endPoint, myRequest));
}

try
{
    Task.WaitAll(taskList.ToArray());
}
catch (Exception ex)
{
}

public Task<JObject> GetResponse(string endPoint, string myRequest)
{
    return Task.Run(() =>
        {
            HttpClient httpClient = new HttpClient();

            HttpResponseMessage response = httpClient.PostAsJsonAsync<string>(
                 string.Format("{0}api/GetResponse", endpoint), 
                 myRequest, 
                 new CancellationTokenSource(TimeSpan.FromMilliseconds(5)).Token);

            JObject resultResponse = response.Content.ReadAsAsync<JObject>();
        });
}

如果你把一行代码放在下面这个语句之后:Task.WaitAll(taskList.ToArray());它会在不到1秒钟内被执行吗?在我的情况下,它需要20秒才能被执行。我希望能够使用超时机制。 我在这里提出了一个类似的问题并询问了你提供的解决方案: http://stackoverflow.com/questions/29102274/c-sharp-async-await-calls-using-httpclient-with-timeout - Alberto Montellano
仅仅因为你试图同时启动300个线程,并不意味着它们会真正同时运行。它们将被线程池限制 - 有些线程在其他线程完成之前不会启动。记录活动线程的数量,以了解我的意思。 - RagtimeWilly
没错,但是在这个示例中,如果超时时间到期了,它会返回一个异常,如果引发了异常,我们将"什么都不做"(在我的情况下计划返回一个默认的空JObject),因此线程应该非常快地释放。假设我们不使用HttpClient,则线程完成得非常快。现在,如果我们使用HttpClient并调用方法,无论我们为其定义的超时时间如何,它都需要很长时间。这就是我要问的。 - Alberto Montellano
7
HttpClient 的操作是 I/O 绑定的,本质上是异步的。通过使用 Task.Run 强制每个调用在不同的线程上运行,在效率或总体速度方面并没有任何收益。如果有的话,它可能会造成损失。 - Todd Menier
1
看起来你并没有为每个请求实际上运行一个独立的线程。不需要使用单独的线程批量处理请求。请参考@Todd Menier的回答。 http://blog.stephencleary.com/2013/11/there-is-no-thread.html - Jerry Joseph
显示剩余6条评论

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