HttpClient SendAsync 阻塞主线程

Posted

技术标签:

【中文标题】HttpClient SendAsync 阻塞主线程【英文标题】:HttpClient SendAsync blocks main thread 【发布时间】:2020-08-20 21:23:58 【问题描述】:

我编写了一个小的 winforms 应用程序,它将 http 请求发送到我本地网络中的每个 IP 地址,以发现我的某个设备。在我的特定子网掩码上,那是 512 个地址。我已经使用 backGroundWorker 编写了这个,但我想尝试 httpClient 和 Async/Await 模式来实现相同的目标。下面的代码使用一个 httpClient 实例,我一直等到所有请求都完成。这个问题是主线程被阻塞了。我知道这一点是因为我有一个图片框 + 加载 gif 并且它的动画不统一。我按照建议 here 将 GetAsync 方法放在 Task.Run 中,但这也不起作用。

    private async void button1_Click(object sender, EventArgs e)
    
        var addresses = networkUtils.generateIPRange..
        await MakeMultipleHttpRequests(addresses);
    


    public async Task MakeMultipleHttpRequests(IPAddress[] addresses)
    
        List<Task<HttpResponseMessage>> httpTasks = new List<Task<HttpResponseMessage>>();
        foreach (var address in addresses)
        
            Task<HttpResponseMessage> response = MakeHttpGetRequest(address.ToString());
            httpTasks.Add(response);
        
        try
        
            if (httpTasks.ToArray().Length != 0)
            
                await Task.WhenAll(httpTasks.ToArray());
            
        
        catch (Exception ex)
        
            Console.WriteLine("\thttp tasks did not complete Exception : 0", ex.Message);
        

    

    private async Task<HttpResponseMessage> MakeHttpGetRequest(string address)
    
        var url = string.Format("http://0/getStatus", address);
        var cts = new System.Threading.CancellationTokenSource();
        cts.CancelAfter(TimeSpan.FromSeconds(10));
        HttpResponseMessage response = null;
        var request = new HttpRequestMessage(HttpMethod.Get, url);
        response = await httpClient.SendAsync(request, cts.Token);
        return response;
    

我读过一个类似的问题here,但我的 gui 线程没有做太多。我读过here 我可能用完了线程。是这个问题吗,我该如何解决? 我知道它是 Send Async,因为如果我用下面的简单任务替换代码,就不会阻塞。

    await Task.Run(() =>
    
       Thread.Sleep(1000);
    );

【问题讨论】:

我知道发生了什么。您有 500 多个 Http 请求一次都抛出超时异常......当它被锁定时,请注意您的“输出”窗口。 我认为 OP 意味着在 UI 线程上完成了一些工作,并且它中断了 gif 的流畅动画。是的,因为您正在使用 async 并等待它实际上并不意味着线程,并且延续将在 UI 上下文中运行。如果您想确保一切都与 ui 完全隔离,我建议卸载。 Task.Run(一切) 是的,我确实有超时异常,我尝试在 Send Async 周围捕获,但它们不应该在自己的线程上安全处理吗? @JoePhillips 阻塞的意思,加载微调器 gif 卡住了,我也无法点击任何 ui 按钮。 我在说什么:它被锁定了,因为你处于调试模式并且调试器试图一次说“嘿,你有一个异常”500 次。在 Release 模式下运行它,看看它是否仍然锁定。另外,你做错了超时。 @Andy ,是的,有效。 gif 不是很流畅,但要好得多,我很高兴接受这个作为答案。您的意思是取消令牌的设置有误吗? 【参考方案1】:

因此,这里的问题之一是您正在快速连续地创建 500 多个任务,并且在任务创建之外设置了超时。

仅仅因为您要求运行 500 多个任务,并不意味着 500 多个任务都将同时运行。当调度程序认为可能时,它们会排队并运行。

您在创建时设置了 10 秒的超时时间。但它们甚至可以在执行之前在调度程序中停留 10 秒。

您希望 Http 请求自然地超时,您可以在创建 HttpClient 时这样做:

private static readonly HttpClient _httpClient = new HttpClient

    Timeout = TimeSpan.FromSeconds(10)
;

因此,通过将超时移至 HttpClient,您的方法现在应该如下所示:

private static Task<HttpResponseMessage> MakeHttpGetRequest(string address)

    return _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, new UriBuilder
    
        Host = address,
        Path = "getStatus"
    .Uri));

尝试使用该方法,看看它是否可以改善您在调试模式下的锁定问题。

就您遇到的问题而言:它被锁定是因为您处于调试模式,并且调试器试图说“嘿,您遇到了异常”500 次所有在同一时间,因为它们都是同时生成的。在 Release 模式下运行它,看看它是否仍然锁定。

我会考虑做的是批量处理您的操作。做 20 个,然后等到这 20 个完成,再做 20 个,依此类推。

如果您想了解一种巧妙的批处理任务方式,请告诉我,我很乐意向您展示。

【讨论】:

使用 SemaphoreSlim 限制请求可能是解决此问题的最佳方法 @JoePhillips -- 绝对,如果他想看一个例子,那正是我会使用的。带有大约 20 个插槽的信号量。 或 TPL ActionBlock。 @Andy,谢谢,是的,在发布模式下,一切正常。我尝试按照您的建议设置 httpClient 超时,但它在调试模式下没有任何区别,我想这是因为 99% 的调用总是以异常结束。请给出一个做批处理操作的例子,那很有趣。将此标记为答案。【参考方案2】:

在 .NET Framework 上,与服务器的连接数由 ServicePointManager Class 控制。

对于客户端,the default connection limit 在客户端进程上是 2

无论您执行多少次 HttpClient.SendAsync 调用,只有 2 个会同时处于活动状态。

但你可以manage the connections自己。

在 .NET Core 上,这里没有服务点管理器的概念,等效的默认限制是 int.MaxValue。

【讨论】:

这看起来很有趣。您是否有更多关于何时/如何有用的信息?也许是一个例子? 在 ASP.NET 服务器应用程序上,这个数字会更高,因为它更有可能是对同一主机/URL 的更多并发请求。在客户端应用程序上,当您需要超过 2 个与同一主机/URL 的连接时,它会很有用。该系统是非常可配置的。请查看提供的链接和这些文档的链接中的文档。 读起来很有趣,谢谢。就我而言,我正在对多个主机进行一次调用,所以我认为我没有达到所描述的限制。我确实尝试过,但没有任何区别。但是,当我的应用程序这样做时,我会记得将其设置为 2 以上。 您是否已确认确实已建立连接?使用netsat 还是 Fiddler? 您会同时看到所有这 512 个与服务器的连接吗?

以上是关于HttpClient SendAsync 阻塞主线程的主要内容,如果未能解决你的问题,请参考以下文章

HttpClient.SendAsync() 未将 JSON 作为字符串发送

HttpClient.SendAsync 不发送请求正文

HttpClient.SendAsync 方法退出而不抛出异常

HttpClient SendAsync

模拟 HttpClient.SendAsync 以返回内容不为空的响应

如何从 Httpclient.SendAsync 调用中获取和打印响应