Por que o cancelamento bloqueia por tanto tempo ao cancelar muitas solicitações HTTP?

fundo

Eu tenho um código que executa o processamento de páginas HTML em lote usando o conteúdo de um host específico. Ele tenta fazer um grande número (~ 400) de solicitações HTTP simultâneas usandoHttpClient. Eu acredito que o número máximo de conexões simultâneas é restrito porServicePointManager.DefaultConnectionLimit, portanto, não estou aplicando minhas próprias restrições de concorrência.

Depois de enviar todos os pedidos de forma assíncrona paraHttpClient usandoTask.WhenAll, toda a operação em lote pode ser cancelada usandoCancellationTokenSource eCancellationToken. O progresso da operação é visível através de uma interface de usuário, e um botão pode ser clicado para realizar o cancelamento.

Problema

A chamada paraCancellationTokenSource.Cancel() blocos por aproximadamente 5 a 30 segundos. Isso faz com que a interface do usuário congele. É suspeito que isso ocorra porque o método está chamando o código registrado para notificação de cancelamento.

O que eu consideroLimitando o número de tarefas de solicitação HTTP simultâneas. Eu considero isso uma solução porqueHttpClient já parece colocar em fila as solicitações em excesso.Executando oCancellationTokenSource.Cancel() chamada de método em um thread não-UI. Isso não funcionou muito bem; a tarefa não foi executada até que a maioria das outras terminasse. Acho que umasync A versão do método funcionaria bem, mas não consegui encontrar uma. Além disso, tenho a impressão de que é adequado usar o método em um thread de interface do usuário.DemonstraçãoCódigo
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();
    }
}
Saída

Por que o bloqueio de cancelamento por tanto tempo? Além disso, há alguma coisa que eu esteja fazendo errado ou poderia estar fazendo melhor?

questionAnswers(1)

yourAnswerToTheQuestion