HttpClient查询有时会挂起

时间:2018-12-25 20:11:29

标签: c# timeout httpclient

我像这样初始化HttpClient

public static CookieContainer cookieContainer = new CookieContainer();
public static HttpClient httpClient = new HttpClient(new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, CookieContainer = cookieContainer }) { Timeout = TimeSpan.FromSeconds(120) };

因此,如果120秒内未收到响应,则所有查询都应抛出TaskCanceledException。 但是某些查询(例如100 000-1 000 000中的1个)无限挂起。

我写了以下代码:

public static async Task<HttpResponse> DownloadAsync2(HttpRequestMessage httpRequestMessage)
{
    HttpResponse response = new HttpResponse { Success = false, StatusCode = (int)HttpStatusCode.RequestTimeout, Response = "Timeout????????" };
    Task task;
    if (await Task.WhenAny(
        task = Task.Run(async () =>
        {
            try
            {
                HttpResponseMessage r = await Global.httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false);
                response = new HttpResponse { Success = true, StatusCode = (int)r.StatusCode, Response = await r.Content.ReadAsStringAsync().ConfigureAwait(false) };
            }
            catch (TaskCanceledException)
            {
                response = new HttpResponse { Success = false, StatusCode = (int)HttpStatusCode.RequestTimeout, Response = "Timeout" };
            }
            catch (Exception ex)
            {
                response = new HttpResponse { Success = false, StatusCode = -1, Response = ex.Message + ": " + ex.InnerException };
            }
        }),
        Task.Run(async () =>
        {
            await Task.Delay(TimeSpan.FromSeconds(150)).ConfigureAwait(false);
        })
    ).ConfigureAwait(false) != task)
    {
        Log("150 seconds passed");
    }
    return response;
}

实际上偶尔执行Log("150 seconds passed");

我这样称呼它:

HttpResponse r = await DownloadAsync2(new HttpRequestMessage
{
    RequestUri = new Uri("https://address.com"),
    Method = HttpMethod.Get
}).ConfigureAwait(false);

为什么TaskCanceledException有时在120秒后仍未抛出?

6 个答案:

答案 0 :(得分:2)

我不知道您叫DownloadAsync2的频率,但是您的代码在爆裂和使ThreadPool饿死时闻起来很多。

默认情况下,ThreadPool中的线程初始数量仅限于CPU逻辑内核的数量(对于当今的普通系统通常为12个),并且在ThreadPool中的线程不可用的情况下,每个新线程的生成需要500ms。

例如:

for (int i = 0; i < 1000; i++)
{
    HttpResponse r = await DownloadAsync2(new HttpRequestMessage
    {
        RequestUri = new Uri("https://address.com"),
        Method = HttpMethod.Get
    }).ConfigureAwait(false);
}

此代码很有可能会被冻结,特别是如果您在代码中的某个地方有某些lock或任何cpu intensive任务时。因为您在每次调用DownloadAsync2时都调用了新线程,所以ThreadPool的所有线程都被消耗了,还需要更多线程。

我知道您也许会说:“ 我所有的任务都在等待中,并且可以用于其他工作”。但是它们也消耗了启动新的DownloadAsync2线程的时间,并且您将得出结论,在完成await Global.httpClient.SendAsync之后,没有线程可用于重新分配和完成任务。

所以方法必须等到一个线程可用或生成完成(即使在超时之后)。稀有但可行。

答案 1 :(得分:1)

我发现这是httpClient.SendAsync方法,有时会挂起。因此,我添加了一个设置为X秒的取消令牌。但是,即使使用取消标记,它有时仍会卡住并且永远不会抛出TaskCanceledException

因此,我着手解决此问题,使SendAsync任务永远停留在后台,并继续进行其他工作。

这是我的解决方法:

public static async Task<Response> DownloadAsync3(HttpRequestMessage httpRequestMessage, string caller)
{
    Response response;
    try
    {
        using CancellationTokenSource timeoutCTS = new CancellationTokenSource(httpTimeoutSec * 1000);
        using HttpResponseMessage r = await Global.httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseContentRead, timeoutCTS.Token).WithCancellation(timeoutCTS.Token).ConfigureAwait(false);
        response = new Response { Success = true, StatusCode = (int)r.StatusCode, Message = await r.Content.ReadAsStringAsync().ConfigureAwait(false) };
    }
    catch (TaskCanceledException)
    {
        response = new Response { Success = false, StatusCode = (int)HttpStatusCode.RequestTimeout, Message = "Timeout" };
    }
    catch (Exception ex)
    {
        response = new Response { Success = false, StatusCode = -1, Message = ex.Message + ": " + ex.InnerException };
    }
    httpRequestMessage.Dispose();
    return response;
}

public static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    return task.IsCompleted
        ? task
        : task.ContinueWith(
            completedTask => completedTask.GetAwaiter().GetResult(),
            cancellationToken,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
}

答案 2 :(得分:1)

使用Flurl,您可以配置每个客户端,每个请求或全局的超时。


// call once at application startup
FlurlHttp.Configure(settings => settings.Timeout = TimeSpan.FromSeconds(120));

string url = "https://address.com";

// high level scenario
var response = await url.GetAsync();

// low level scenario
await url.SendAsync(
    HttpMethod.Get, // Example
    httpContent, // optional
    cancellationToken,  // optional
    HttpCompletionOption.ResponseHeaderRead);  // optional

// Timeout at request level
await url
    .WithTimeout(TimeSpan.FromSeconds(120))
    .GetAsync();

Fluent HTTP documentation

Flurl configuration documentation

答案 3 :(得分:1)

在Windows和.NET上,到同一终结点的并发传出HTTP请求的数量限制为2(根据HTTP 1.1规范)。如果您向同一端点创建大量并发请求,则它们将排队。那是对您所经历的一种可能的解释。

另一个可能的解释是:您没有显式设置HttpClient的Timeout属性,因此默认值为100秒。如果您继续发出新请求,而先前的请求没有完成,则系统资源将被耗尽。

我建议将Timeout属性设置为一个较低的值-与您拨打电话的频率成正比(1秒?),并可以选择使用ServicePointManager.DefaultConnectionLimit

增加并发传出连接的数量。

答案 4 :(得分:0)

答案是:

ThreadPool.SetMinThreads(MAX_THREAD_COUNT, MAX_THREAD_COUNT);

其中MAX_THREAD_COUNT是一些数字(我用200)。您必须至少设置第二个参数(completionPortThreads),并且很可能设置第一个参数(workerThreads)。我已经设置了第一个,但没有设置第二个,现在它可以正常工作了,我将两个设置都保持不变。

A,这不是答案。查看下面的评论

答案 5 :(得分:0)

好的,我正式放弃!我将代码替换为:

    try
    {
        return Task.Run(() => httpClient.SendAsync(requestMessage)).Result;
    }
    catch (AggregateException e)
    {
        if (e.InnerException != null)
            throw e.InnerException;
        throw;
    }