Pourquoi les annulations bloquent-elles si longtemps lors de l’annulation de nombreuses requêtes HTTP?

Contexte

J’ai du code qui effectue un traitement de page HTML par lot en utilisant le contenu d’un hôte spécifique. Il essaie de créer un grand nombre (~ 400) de requêtes HTTP simultanées à l’aide de HttpClient . Je pense que ServicePointManager.DefaultConnectionLimit limite le nombre maximal de connexions simultanées. Par conséquent, je n’applique pas mes propres ressortingctions d’access simultané.

Après avoir envoyé toutes les demandes de manière asynchrone à HttpClient aide de Task.WhenAll , la totalité de l’opération de traitement par lots peut être annulée à l’aide de CancellationTokenSource et CancellationToken . La progression de l’opération est visible via une interface utilisateur et un bouton peut être cliqué pour effectuer l’annulation.

Problème

L’appel de CancellationTokenSource.Cancel() bloqué pendant environ 5 à 30 secondes. Cela provoque le gel de l’interface utilisateur. Cela est suspecté car la méthode appelle le code qui a été enregistré pour la notification d’annulation.

Ce que j’ai considéré

  1. Limiter le nombre de tâches de requête HTTP simultanées. Je considère cela comme une HttpClient contournement, car HttpClient semble déjà mettre en queue les demandes excédentaires.
  2. Exécution de l’appel de la méthode CancellationTokenSource.Cancel() dans un thread autre que l’interface utilisateur. Cela n’a pas très bien fonctionné. la tâche n’a pas été exécutée jusqu’à ce que la plupart des autres aient terminé. Je pense qu’une version async de la méthode fonctionnerait bien, mais je ne pouvais pas en trouver une. De plus, j’ai l’impression qu’il est approprié d’utiliser la méthode dans un thread d’interface utilisateur.

Manifestation

Code

 class Program { private const int desiredNumberOfConnections = 418; static void Main(ssortingng[] 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 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(); } } 

Sortie

Fenêtre de la console affichant l'annulation bloquée pendant plus de 13 secondes

Pourquoi l’annulation bloque-t-elle si longtemps? Aussi, y a-t-il quelque chose que je fais mal ou que je pourrais faire mieux?

Exécution de l’appel de la méthode CancellationTokenSource.Cancel () dans un thread autre que l’interface utilisateur. Cela n’a pas très bien fonctionné. la tâche n’a pas été exécutée jusqu’à ce que la plupart des autres aient terminé.

Cela me dit que vous souffrez probablement de “épuisement du pool de threads”, c’est-à-dire où votre queue de threadpool contient tellement d’éléments (à partir de requêtes HTTP terminées) qu’il faut un certain temps pour les traiter tous. L’annulation bloque probablement l’exécution d’un élément de travail d’un pool de threads qui ne peut pas aller directement au début de la queue.

Cela suggère que vous devez utiliser l’option 1 de votre liste de considération. Limitez votre travail afin que la file de threadpool rest relativement courte. De toute façon, c’est bon pour la réactivité de l’application.

Ma méthode préférée pour limiter le travail asynchrone consiste à utiliser Dataflow . Quelque chose comme ça:

 var block = new ActionBlock( 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. 

Alternativement, vous pouvez utiliser Task.Factory.StartNew en passant à TaskCreationOptions.LongRunning afin que votre tâche obtienne un nouveau thread (non affilié à threadpool) qui lui permettrait de démarrer immédiatement et d'appeler Annuler à partir de là. Mais vous devriez plutôt résoudre le problème d’épuisement du pool de threads.