Est-il vrai qu’async ne doit pas être utilisé pour des tâches à processeur élevé?

Je me demandais s’il était vrai asyncawait ne devrait pas être utilisé pour des tâches “à processeur élevé”. J’ai vu cela revendiqué dans une présentation.

Donc, je suppose que cela voudrait dire quelque chose comme

 Task calculateMillionthPrimeNumber = CalculateMillionthPrimeNumberAsync(); DoIndependentWork(); int p = await calculateMillionthPrimeNumber; 

Ma question est la suivante: est -ce que ce qui précède peut être justifié? Sinon, existe-t-il un autre exemple de création asynchrone d’une tâche à processeur élevé?

Je me demandais s’il était vrai que l’attente asynchrone ne devrait pas être utilisée pour des tâches “à processeur élevé”.

Oui c’est vrai.

Ma question est: est-ce que ce qui précède pourrait être justifié?

Je dirais que ce n’est pas justifié. Dans le cas général, vous devriez éviter d’utiliser Task.Run pour implémenter des méthodes avec des signatures asynchrones. N’exposez pas les wrappers asynchrones pour les méthodes synchrones . Cela évite toute confusion chez les consommateurs, en particulier sur ASP.NET.

Cependant, l’utilisation de Task.Run pour appeler une méthode synchrone, par exemple dans une application d’interface utilisateur, ne présente aucun Task.Run . De cette manière, vous pouvez utiliser le multithreading ( Task.Run ) pour garder le thread d’interface utilisateur libre et le consumr avec élégance avec await :

 var task = Task.Run(() => CalculateMillionthPrimeNumber()); DoIndependentWork(); var prime = await task; 

Il existe en fait deux utilisations principales de async / wait. L’une (et j’ai cru comprendre que c’est l’une des principales raisons pour lesquelles elle a été intégrée dans la structure) consiste à permettre au fil d’appel d’effectuer d’autres tâches en attendant un résultat. Ceci concerne principalement les tâches liées aux E / S (c.-à-d. Les tâches pour lesquelles le “blocage” principal est une sorte d’E / S – en attente d’un disque dur, d’un serveur, d’une imprimante, etc. pour répondre ou terminer sa tâche).

En remarque, si vous utilisez async / wait de cette manière, il est important de vous assurer que vous l’avez implémenté de manière à ce que le fil d’appel puisse réellement effectuer un autre travail en attendant le résultat. J’ai vu beaucoup de cas où des gens font des choses comme “A attend B, qui attend C”; cela peut ne pas donner de meilleurs résultats que si A appelait simplement B de manière synchrone et B appelait simplement C de manière synchrone (car le thread appelant n’a jamais été autorisé à effectuer un autre travail pendant qu’il attend les résultats de B et C).

Dans le cas de tâches liées aux E / S, il est inutile de créer un thread supplémentaire juste pour attendre un résultat. Mon analogie habituelle ici est de penser à commander dans un restaurant avec 10 personnes dans un groupe. Si la première personne à qui le serveur demande de passer commande n’est pas encore prête, le serveur n’attend pas qu’il soit prêt avant de prendre la commande de quelqu’un d’autre, il ne fait pas venir un deuxième serveur juste pour attendre le premier. La meilleure chose à faire dans ce cas est de demander les 9 autres membres du groupe pour leurs ordres; j’espère que, au moment où ils auront commandé, le premier type sera prêt. Sinon, au moins, le serveur gagne encore du temps, car il passe moins de temps en veille.

Il est également possible d’utiliser des Task.Run telles que Task.Run pour effectuer des tâches liées au processeur (et c’est la deuxième utilisation pour cela). Pour suivre notre analogie ci-dessus, c’est un cas où il serait généralement utile d’avoir plus de serveurs – par exemple s’il y avait trop de tables pour un seul serveur à desservir. Vraiment, tout ce que cela fait réellement “en coulisse” est d’utiliser le pool de threads; c’est l’une des constructions possibles pour effectuer un travail lié au processeur (par exemple, le placer “directement” sur le pool de threads, créer explicitement un nouveau thread ou utiliser un arrière – plan ), de sorte que le mécanisme que vous utiliserez est une question de conception.

async/await avantage de pouvoir, dans certaines circonstances, réduire la quantité de logique de locking / synchronisation explicite que vous devez écrire manuellement. Voici une sorte d’exemple stupide:

 private static async Task SomeCPUBoundTask() { // Insert actual CPU-bound task here using Task.Run await Task.Delay(100); } public static async Task QueueCPUBoundTasks() { List tasks = new List(); // Queue up however many CPU-bound tasks you want for (int i = 0; i < 10; i++) { // We could just call Task.Run(...) directly here Task task = SomeCPUBoundTask(); tasks.Add(task); } // Wait for all of them to complete // Note that I don't have to write any explicit locking logic here, // I just tell the framework to wait for all of them to complete await Task.WhenAll(tasks); } 

Évidemment, je suppose ici que les tâches sont complètement parallélisables. Notez également que vous auriez pu utiliser le pool de threads vous-même ici, mais ce serait un peu moins pratique car vous auriez besoin d’un moyen de déterminer vous-même si tous les travaux étaient terminés (au lieu de simplement laisser le cadre le déterminer pour vous). Vous avez peut-être également pu utiliser une boucle Parallel.For ici.

Supposons que votre CalculateMillionthPrimeNumber ressemble à ce qui suit (pas très efficace ou idéal dans son utilisation de goto mais très simple à comprendre):

 public int CalculateMillionthPrimeNumber() { List primes = new List(1000000){2}; int num = 3; while(primes.Count < 1000000) { foreach(int div in primes) { if ((num / div) * div == num) goto next; } primes.Add(num); next: ++num; } return primes.Last(); } 

Or, il n’ya pas de point utile ici pour que cela puisse faire quelque chose de manière asynchrone. Faisons-en une méthode de retour de tâche utilisant async :

 public async Task CalculateMillionthPrimeNumberAsync() { List primes = new List(1000000){2}; int num = 3; while(primes.Count < 1000000) { foreach(int div in primes) { if ((num / div) * div == num) goto next; } primes.Add(num); next: ++num; } return primes.Last(); } 

Le compilateur nous en avertira, car nous ne pouvons rien await d’utile. Vraiment appeler cela va être identique à une version légèrement plus compliquée de l'appel de Task.FromResult(CalculateMillionthPrimeNumber()) . Cela revient à faire le calcul puis à créer une tâche déjà terminée ayant pour résultat le nombre calculé.

Maintenant, les tâches déjà terminées ne sont pas toujours inutiles. Par exemple, considérons:

 public async Task GetInterestingSsortingngAsync() { if (_cachedInterestingSsortingng == null) _cachedInterestingSsortingng = await GetSsortingngFromWeb(); return _cachedInterestingSsortingng; } 

Cela retourne une tâche déjà terminée lorsque la chaîne est dans le cache, et non autrement, et dans ce cas, le retour sera assez rapide. D'autres cas peuvent se présenter s'il existe plusieurs implémentations de la même interface et si toutes les implémentations ne peuvent pas utiliser les E / S asynchrones.

Et de même, une méthode async qui await que cette méthode retourne une tâche déjà terminée ou ne dépend pas de cela. C'est en fait un excellent moyen de restr sur le même sujet et de faire ce qui doit être fait quand c'est possible.

Mais si cela est toujours possible, le seul effet est de créer un object tordu et de créer la machine à états async par async pour le mettre en œuvre.

Donc, assez inutile. Si c'était ainsi que la version de votre question avait été implémentée, alors IsCompleted aurait IsCompleted True à IsCompleted dès le début. Vous devriez juste appeler la version non asynchrone.

OK, en tant qu'implémenteurs de CalculateMillionthPrimeNumberAsync() nous souhaitons apporter quelque chose de plus utile à nos utilisateurs. Donc nous faisons:

 public Task CalculateMillionthPrimeNumberAsync() { return Task.Factory.StartNew(CalculateMillionthPrimeNumber, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } 

Ok, maintenant nous ne perdons pas le temps de nos utilisateurs. DoIndependentWork() fera des choses en même temps que CalculateMillionthPrimeNumberAsync() , et si elle se termine en premier, l' await publiera ce thread.

Génial!

Seulement, nous n'avons pas vraiment déplacé l'aiguille de la position synchrone. En effet, surtout si DoIndependentWork() n’est pas ardu, nous avons peut-être aggravé la situation. La manière synchrone ferait tout sur un thread, appelons-le Thread A La nouvelle méthode effectue le calcul sur le Thread B puis libère le Thread A , puis se synchronise de plusieurs manières. C'est beaucoup de travail, a-t-il gagné quelque chose?

Peut-être bien, mais l'auteur de CalculateMillionthPrimeNumberAsync() ne peut pas le savoir, car les facteurs qui influencent tout sont dans le code d'appel. Le code appelant aurait pu être utilisé par StartNew lui-même et être mieux en mesure d'adapter les options de synchronisation au besoin quand il l'a fait.

Ainsi, bien que les tâches puissent constituer un moyen pratique d’appeler du code lié à cpu en parallèle à une autre tâche, les méthodes qui le font ne sont pas utiles. Pire, ils trompent le fait que quelqu'un qui voit CalculateMillionthPrimeNumberAsync pourrait être pardonné de croire que cet appel n'était pas inutile.

Sauf si CalculateMillionthPrimeNumberAsync utilise constamment async/await par lui-même, il n’y a aucune raison de ne pas laisser la tâche exécuter un travail de processeur lourd, car elle ne fait que déléguer votre méthode au thread de ThreadPool.

Qu’est-ce qu’un thread ThreadPool et en quoi diffère-t-il d’un thread ordinaire?

En bref, le thread threadpool est en garde pendant un bon bout de temps (et le nombre de thread threadpool est limité). Ainsi, à moins que vous n’en preniez trop, il n’y a rien à craindre.