Dlaczego anulowanie blokuje się na tak długo przy anulowaniu wielu żądań HTTP?

tło

Mam jakiś kod, który wykonuje wsadowe przetwarzanie strony HTML przy użyciu treści z jednego konkretnego hosta. Próbuje utworzyć dużą liczbę (~ 400) jednoczesnych żądań HTTP za pomocąHttpClient. Uważam, że maksymalna liczba jednoczesnych połączeń jest ograniczona przezServicePointManager.DefaultConnectionLimit, więc nie stosuję własnych ograniczeń współbieżności.

Po wysłaniu wszystkich żądań asynchronicznie doHttpClient za pomocąTask.WhenAll, całą operację wsadową można anulować za pomocąCancellationTokenSource iCancellationToken. Postęp operacji jest widoczny za pośrednictwem interfejsu użytkownika, a przycisk można kliknąć, aby wykonać anulowanie.

Problem

Wezwanie doCancellationTokenSource.Cancel() bloki przez około 5 - 30 sekund. Powoduje to zamrożenie interfejsu użytkownika. Podejrzewa się, że dzieje się tak, ponieważ metoda wywołuje kod zarejestrowany w celu powiadomienia o anulowaniu.

Co rozważałemOgraniczenie liczby jednoczesnych zadań żądania HTTP. Uważam to za obejście, ponieważHttpClient zdaje się już ustawiać w kolejce nadmiarowe żądania.WykonywanieCancellationTokenSource.Cancel() wywołanie metody w wątku innym niż UI. To nie zadziałało zbyt dobrze; zadanie nie działało, dopóki większość pozostałych nie skończyła. Myślęasync wersja metody działa dobrze, ale nie mogłem jej znaleźć. Odnoszę również wrażenie, że można użyć tej metody w wątku interfejsu użytkownika.DemonstracjaKod
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();
    }
}
Wydajność

Dlaczego blokowanie trwa tak długo? Czy jest coś, co robię źle, czy może lepiej?

questionAnswers(1)

yourAnswerToTheQuestion