C# 当取消大量HTTP请求时,为什么取消会阻塞这么长时间? 背景

C# 当取消大量HTTP请求时,为什么取消会阻塞这么长时间? 背景,c#,performance,.net-4.5,c#-5.0,cancellationtokensource,C#,Performance,.net 4.5,C# 5.0,Cancellationtokensource,我有一些代码,它使用来自一个特定主机的内容执行批量HTML页面处理。它尝试使用HttpClient同时发出大量(~400)HTTP请求。我认为同时连接的最大数量受到ServicePointManager.DefaultConnectionLimit的限制,因此我没有应用自己的并发限制 使用Task将所有请求异步发送到HttpClient后。所有时,可以使用CancellationTokenSource和CancellationToken取消整个批处理操作。可通过用户界面查看操作的进度,并可单击按

我有一些代码,它使用来自一个特定主机的内容执行批量HTML页面处理。它尝试使用
HttpClient
同时发出大量(~400)HTTP请求。我认为同时连接的最大数量受到
ServicePointManager.DefaultConnectionLimit
的限制,因此我没有应用自己的并发限制

使用
Task将所有请求异步发送到
HttpClient
后。所有
时,可以使用
CancellationTokenSource
CancellationToken
取消整个批处理操作。可通过用户界面查看操作的进度,并可单击按钮执行取消操作

问题 调用
CancellationTokenSource.Cancel()
会阻塞大约5-30秒。这会导致用户界面冻结。怀疑发生这种情况是因为该方法正在调用为取消通知注册的代码

我所考虑的
  • 限制同步HTTP请求任务的数量。我认为这是一个工作,因为<代码> HTTPcli客< /代码>似乎已经对多余的请求本身进行了排队。
  • 在非UI线程中执行
    CancellationTokenSource.Cancel()
    方法调用。这不太管用;直到其他大多数人都完成了任务,任务才真正开始运行。我认为该方法的
    async
    版本可以很好地工作,但我找不到。另外,我觉得在UI线程中使用该方法是合适的
  • 示范 代码
    类程序
    {
    private const int desiredNumberOfConnections=418;
    静态void Main(字符串[]参数)
    {
    许多前传()等待();
    控制台。WriteLine(“完成”);
    Console.ReadKey();
    }
    私有静态异步任务manyhtprequeststest()
    {
    使用(var client=new HttpClient())
    使用(var cancellationTokenSource=new cancellationTokenSource())
    {
    var requestsCompleted=0;
    使用(var allrequestssstarted=newcountdownEvent(desiredNumberOfConnections))
    {
    Action reportRequestStarted=()=>allRequestsStarted.Signal();
    动作报告RequestCompleted=()=>Interlocked.Increment(ref requestsCompleted);
    Func getHttpResponse=index=>getHttpResponse(客户端,cancellationTokenSource.Token,reportRequestStarted,reportRequestCompleted);
    var httpRequestTasks=Enumerable.Range(0,desiredNumberOfConnections)。选择(getHttpResponse);
    WriteLine(“HTTP请求批处理正在启动”);
    var httpRequestsTask=Task.WhenAll(httpRequestTasks);
    WriteLine(“启动{0}个请求(同时连接限制为{1})”,desiredNumberOfConnections,ServicePointManager.DefaultConnectionLimit);
    allRequestsStarted.Wait();
    取消(cancellationTokenSource);
    等待WaitForRequestsToFinish(httpRequestsTask);
    }
    WriteLine(“{0}HTTP请求已完成”,requestsCompleted);
    }
    }
    私有静态无效取消(CancellationTokenSource CancellationTokenSource)
    {
    控制台。写入(“取消…”);
    var stopwatch=stopwatch.StartNew();
    cancellationTokenSource.Cancel();
    秒表;
    WriteLine(“花费了{0}秒”,秒表.appead.TotalSeconds);
    }
    私有静态异步任务WaitForRequestsToFinish(任务httpRequestsTask)
    {
    WriteLine(“等待HTTP请求完成”);
    尝试
    {
    等待httpRequestsTask;
    }
    捕获(操作取消异常)
    {
    WriteLine(“HTTP请求被取消”);
    }
    }
    私有静态异步任务GetHttpResponse(HttpClient客户端、CancellationToken CancellationToken、Action reportStarted、Action reportFinished)
    {
    var getResponse=client.GetAsync(“http://www.google.com“,取消令牌);
    reportStarted();
    使用(var响应=等待getResponse)
    response.EnsureSuccessStatusCode();
    reportFinished();
    }
    }
    
    输出

    为什么取消会阻塞这么长时间?还有,我有没有做错什么,或者可以做得更好

    在非UI线程中执行CancellationTokenSource.Cancel()方法调用。这不太管用;直到其他大多数人都完成了任务,任务才真正开始运行

    这告诉我的是,您可能正在遭受“线程池耗尽”的痛苦,这就是线程池队列中有太多项目(来自HTTP请求完成)的原因,需要一段时间才能全部完成。取消操作可能会阻塞某些线程池工作项的执行,并且不能跳到队列的头部

    这表明您确实需要从考虑事项列表中选择选项1。限制您自己的工作,使线程池队列保持相对较短。无论如何,这对应用程序的整体响应性是有好处的

    我最喜欢的控制异步工作的方法是使用。大概是这样的:

    var block = new ActionBlock<Uri>(
        async uri => {
            var httpClient = new HttpClient(); // HttpClient isn't thread-safe, so protect against concurrency by using a dedicated instance for each request.
            var result = await httpClient.GetAsync(uri);
            // do more stuff with result.
        },
        new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 20, CancellationToken = cancellationToken });
    for (int i = 0; i < 1000; i++)
        block.Post(new Uri("http://www.server.com/req" + i));
    block.Complete();
    await block.Completion; // waits until everything is done or canceled.
    
    var block=新动作块(
    异步uri=>{
    var httpClient=new httpClient();//httpClient不是线程安全的,因此通过为每个请求使用专用实例来防止并发。
    var result=await-httpClient.GetAsync(uri);
    //用结果做更多的事情。
    },
    新的ExecutionDataflowBlockOptions{MaxDegreeOfParallelism=20,CancellationToken=CancellationToken});
    对于(int i=0;i<1000;i++)
    block.Post(新Uri(“http://www.server.com/req“+i”);
    block.Complete();
    等待区块完成;//等待所有操作完成或取消。
    
    作为替代方案,您可以使用Task.fact
    var block = new ActionBlock<Uri>(
        async uri => {
            var httpClient = new HttpClient(); // HttpClient isn't thread-safe, so protect against concurrency by using a dedicated instance for each request.
            var result = await httpClient.GetAsync(uri);
            // do more stuff with result.
        },
        new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 20, CancellationToken = cancellationToken });
    for (int i = 0; i < 1000; i++)
        block.Post(new Uri("http://www.server.com/req" + i));
    block.Complete();
    await block.Completion; // waits until everything is done or canceled.