为什么取消了很多的HTTP请求时,确实消除块了这么久?(Why does cancellation

2019-07-20 01:02发布

背景

我有利用从一个特定的主机内容一批HTML页面处理一些代码。 它试图使同时使用HTTP请求的大量(〜400) HttpClient 。 我相信,同时连接的最大数目由限制ServicePointManager.DefaultConnectionLimit ,所以我不会将我自己的并发限制。

发送后,所有的请求的异步到HttpClient使用Task.WhenAll ,整批的操作,可以取消CancellationTokenSourceCancellationToken 。 的操作的进度经由用户接口是可见的,一个按钮可以被按下以执行消除。

问题

到呼叫CancellationTokenSource.Cancel() 30秒-为大致5个街区。 这将导致用户界面冻结。 的嫌疑,这是因为该方法被调用一个注册的取消通知的代码。

我已经考虑

  1. 限制的同时HTTP请求的任务数。 我认为这是一个工作,因为周围HttpClient似乎已经排队过剩请求本身。
  2. 执行CancellationTokenSource.Cancel()在非UI线程的方法调用。 这并没有工作也很好; 任务实际上并没有运行,直到其他大部分已经完成。 我认为,一个async版本的方法,将工作做好,但我找不到一个。 另外,我的印象中,这是适合使用的方法,在UI线程。

示范

class Program
{
    private const int desiredNumberOfConnections = 418;

    static void Main(string[] args)
    {
        ManyHttpRequestsTest().Wait();

        Console.WriteLine("Finished.");
        Console.ReadKey();
    }

    private static async Task ManyHttpRequestsTest()
    {
        using (var client = new HttpClient())
        using (var cancellationTokenSource = new CancellationTokenSource())
        {
            var requestsCompleted = 0;

            using (var allRequestsStarted = new CountdownEvent(desiredNumberOfConnections))
            {
                Action reportRequestStarted = () => allRequestsStarted.Signal();
                Action reportRequestCompleted = () => Interlocked.Increment(ref requestsCompleted);
                Func<int, Task> getHttpResponse = index => GetHttpResponse(client, cancellationTokenSource.Token, reportRequestStarted, reportRequestCompleted);
                var httpRequestTasks = Enumerable.Range(0, desiredNumberOfConnections).Select(getHttpResponse);

                Console.WriteLine("HTTP requests batch being initiated");
                var httpRequestsTask = Task.WhenAll(httpRequestTasks);

                Console.WriteLine("Starting {0} requests (simultaneous connection limit of {1})", desiredNumberOfConnections, ServicePointManager.DefaultConnectionLimit);
                allRequestsStarted.Wait();

                Cancel(cancellationTokenSource);
                await WaitForRequestsToFinish(httpRequestsTask);
            }

            Console.WriteLine("{0} HTTP requests were completed", requestsCompleted);
        }
    }

    private static void Cancel(CancellationTokenSource cancellationTokenSource)
    {
        Console.Write("Cancelling...");

        var stopwatch = Stopwatch.StartNew();
        cancellationTokenSource.Cancel();
        stopwatch.Stop();

        Console.WriteLine("took {0} seconds", stopwatch.Elapsed.TotalSeconds);
    }

    private static async Task WaitForRequestsToFinish(Task httpRequestsTask)
    {
        Console.WriteLine("Waiting for HTTP requests to finish");

        try
        {
            await httpRequestsTask;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("HTTP requests were cancelled");
        }
    }

    private static async Task GetHttpResponse(HttpClient client, CancellationToken cancellationToken, Action reportStarted, Action reportFinished)
    {
        var getResponse = client.GetAsync("http://www.google.com", cancellationToken);

        reportStarted();
        using (var response = await getResponse)
            response.EnsureSuccessStatusCode();
        reportFinished();
    }
}

产量

为什么取消块了这么久? 此外,有什么,我做错了还是可以做的更好?

Answer 1:

执行在非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.

作为替代方案,你可以使用Task.Factory.StartNew传递TaskCreationOptions.LongRunning所以你的任务得到一个新的线程(不与线程池关联),这将使其能够立即开始,并呼吁从那里取消。 但是,你也许应该解决的线程池枯竭问题,而不是。



文章来源: Why does cancellation block for so long when cancelling a lot of HTTP requests?