¿Por qué la cancelación se bloquea durante tanto tiempo al cancelar una gran cantidad de solicitudes HTTP?

Fondo

Tengo algún código que realiza el procesamiento de páginas HTML por lotes utilizando contenido de un host específico. Intenta hacer un gran número (~ 400) de solicitudes HTTP simultáneas usandoHttpClient. Creo que el número máximo de conexiones simultáneas está restringido porServicePointManager.DefaultConnectionLimit, entonces no estoy aplicando mis propias restricciones de concurrencia.

Después de enviar todas las solicitudes de forma asíncrona aHttpClient utilizandoTask.WhenAll, la operación de lote completo puede ser cancelada usandoCancellationTokenSource yCancellationToken. El progreso de la operación se puede ver a través de una interfaz de usuario, y se puede hacer clic en un botón para realizar la cancelación.

Problema

La llamada aCancellationTokenSource.Cancel() Bloques durante aproximadamente 5 - 30 segundos. Esto hace que la interfaz de usuario se congele. Es sospechoso que esto ocurra porque el método está llamando al código que se registró para la notificación de cancelación.

Lo que he consideradoLimitar el número de tareas de solicitud HTTP simultáneas. Considero esto una solución porqueHttpClient Ya parece que las solicitudes de exceso de cola en sí.Realizando elCancellationTokenSource.Cancel() Método de llamada en un subproceso no UI. Esto no funcionó muy bien; la tarea no se ejecutó hasta que la mayoría de los otros habían terminado. Creo que unasync La versión del método funcionaría bien, pero no pude encontrar uno. Además, tengo la impresión de que es adecuado usar el método en un subproceso de la interfaz de usuario.DemostraciónCó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();
    }
}
Salida

¿Por qué la cancelación se bloquea durante tanto tiempo? Además, ¿hay algo que estoy haciendo mal o podría estar haciendo mejor?

Respuestas a la pregunta(1)

Su respuesta a la pregunta