Chaque fois que je lis sur async
-await
, l'exemple de cas d'utilisation est toujours celui où il y a une interface utilisateur que vous ne voulez pas geler. Soit tous les livres/tutoriels de programmation sont les mêmes, soit le blocage de l'interface utilisateur est le seul cas de async
-await
que je devrais connaître en tant que développeur.
Existe-t-il des exemples de la façon dont on pourrait utiliser async
-await
pour retirer des avantages de performances dans un algorithme? Comme prenons l'une des questions d'entrevue de programmation classique:
a[0]
, a[1]
, ..., a[n-1]
représentant les chiffres d'un nombre en base 10, recherchez le prochain nombre le plus élevé utilisant les mêmes chiffres1
, 2
, ..., n
avec un numéro manquant, recherchez le numéro manquantExiste-t-il un moyen de faire ceux qui utilisent async
-await
avec des avantages en termes de performances? Et si oui, que faire si vous n'avez qu'un seul processeur? Votre machine ne partage-t-elle pas simplement son temps entre les tâches plutôt que de les faire en même temps?
En cette interview, Eric Lippert a comparé une attente asynchrone avec un cuisinier faisant le petit déjeuner . Cela m'a beaucoup aidé à comprendre les avantages de l'attente asynchrone. Rechercher quelque part au milieu pour 'async-wait'
Supposons qu'un cuisinier doive préparer le petit-déjeuner. Il doit faire griller du pain et faire bouillir des œufs, peut-être aussi faire du thé?
Méthode 1: synchrone. Réalisé par un thread . Vous commencez à griller le pain. Attendez que le pain soit grillé. Retirez le pain. Commencez à faire bouillir l'eau, attendez que l'eau bout et insérez votre œuf. Attendez que l'œuf soit prêt et retirez l'œuf. Commencez à faire bouillir de l'eau pour le thé. Attendez que l'eau soit bouillie et préparez le thé.
Vous verrez toutes les attentes. pendant que le thread attend, il pourrait faire autre chose.
Méthode 2: attente asynchrone, toujours un fil Vous commencez à griller le pain. Pendant que le pain est grillé, vous commencez à faire bouillir de l'eau pour les œufs et le thé. Ensuite, vous commencez à attendre. Lorsque l'une des trois tâches est terminée, vous effectuez la deuxième partie de la tâche, selon la tâche terminée en premier. Donc, si l'eau des œufs bout en premier, vous faites cuire les œufs et attendez à nouveau que toutes les tâches soient terminées.
Dans cette description, une seule personne (vous) fait tout le travail. Un seul thread est impliqué. La bonne chose est que, comme il n'y a qu'un seul thread qui fait les choses, le code semble assez synchrone pour le lecteur et il n'y a pas beaucoup besoin de sécuriser le thread de vos variables.
Il est facile de voir que de cette façon, votre petit-déjeuner sera prêt en moins de temps (et votre pain sera toujours chaud!). Dans la vie informatique, ces choses se produisent lorsque votre thread doit attendre la fin d'un autre processus, comme écrire un fichier sur un disque, obtenir des informations d'une base de données ou d'Internet. Ce sont généralement les types de fonctions où une version asynchrone de la fonction s'affiche: Write
et WriteAsync
, Read
et ReadAsync
.
Addition: après quelques remarques d'autres utilisateurs d'ailleurs, et quelques tests, j'ai constaté qu'en fait ce peut être n'importe quel thread qui continue votre travail après l'attente. Cet autre thread a le même "contexte" et peut donc agir comme s'il s'agissait du thread d'origine.
Méthode 3: Embauchez des cuisiniers pour griller le pain et faire bouillir les œufs pendant que vous préparez le thé: vraiment asynchrone. Plusieurs threads C'est l'option la plus chère, car elle implique la création de threads séparés. Dans l'exemple de la préparation du petit-déjeuner, cela n'accélérera probablement pas beaucoup le processus, car pendant des périodes relativement importantes, vous ne faites rien de toute façon. Mais si, par exemple, vous devez également trancher des tomates, il peut être utile de laisser un cuisinier (fil distinct) le faire pendant que vous faites les autres choses en utilisant async-wait. Bien sûr, l'une des attentes que vous faites est d'attendre que le cuisinier finisse de trancher.
Un autre article qui explique beaucoup de choses est Async and Await écrit par le très utile Stephen Cleary.
Chaque fois que je lis sur async-wait, l'exemple de cas d'utilisation est toujours celui où il y a une interface utilisateur que vous ne voulez pas geler.
C'est le cas d'utilisation le plus courant pour async
. L'autre se trouve dans les applications côté serveur, où async
peut augmenter l'évolutivité des serveurs Web.
Existe-t-il des exemples de la façon dont on pourrait utiliser async-wait pour retirer des avantages de performances dans un algorithme?
Non.
Vous pouvez utiliser la bibliothèque parallèle de tâches si vous souhaitez effectuer un traitement parallèle. Le traitement parallèle est l'utilisation de plusieurs threads, divisant des parties d'un algorithme entre plusieurs cœurs dans un système. Le traitement parallèle est une forme de concurrence (faire plusieurs choses en même temps).
Le code asynchrone est complètement différent. Le point du code asynchrone est de ne pas utiliser le thread actuel pendant que l'opération est en cours. Le code asynchrone est généralement lié à des E/S ou basé sur des événements (comme un minuteur). Le code asynchrone est une autre forme de concurrence.
J'ai une async
intro sur mon blog, ainsi qu'un article sur comment async
n'utilise pas de threads .
Notez que les tâches utilisées par la bibliothèque parallèle de tâches peuvent être planifiées sur des threads et exécuteront du code. Les tâches utilisées par le modèle asynchrone basé sur les tâches n'ont pas de code et ne "s'exécutent" pas. Bien que les deux types de tâche soient représentés par le même type (Task
), ils sont créés et utilisés complètement différemment; Je décris ces tâches déléguées et tâches promises plus en détail sur mon blog.
En bref et très général - Non, ce n'est généralement pas le cas. Mais cela demande quelques mots de plus, car la "performance" peut être comprise de plusieurs façons.
L'asynchronisation/attente "fait gagner du temps" uniquement lorsque le "travail" est lié aux E/S. Toute application de celui-ci à des travaux qui sont liés au processeur introduira des résultats de performances. En effet, si vous avez des calculs qui prennent 10 secondes par exemple sur votre (vos) CPU, alors ajouter asynchrone/attendre - c'est-à-dire: création de tâches, planification et synchronisation - ajoutera simplement X temps supplémentaire à ces 10 secondes sur lesquelles vous devez encore graver. votre CPU (s) pour faire le travail. Quelque chose de proche de l'idée de la loi Amdahl. Pas vraiment, mais assez proche.
Cependant, il y a des "mais".
Tout d'abord, le fait que les performances atteignent souvent en raison de l'introduction de l'async/wait ne sont pas si importants. (surtout si vous faites attention à ne pas en faire trop).
Deuxièmement, comme async/wait vous permet d'écrire beaucoup plus facilement du code entrelacé d'E/S, vous remarquerez peut-être de nouvelles opportunités de supprimer les temps d'attente sur les E/S dans des endroits où vous seriez trop paresseux (:)) pour le faire autrement ou dans des endroits où cela rendrait le code difficile à suivre sans la syntaxe asynchrone/wait. Par exemple, le fractionnement du code autour des requêtes réseau est une chose assez évidente à faire, mais vous remarquerez peut-être que vous pouvez également mettre à niveau certaines entrées/sorties de fichiers à ces quelques endroits où vous écrivez des fichiers CSV ou lisez des fichiers de configuration, etc. que le gain ici ne sera pas dû à async/wait - ce sera grâce à la réécriture du code qui gère les E/S des fichiers. Vous pouvez le faire sans asynchrone/attendre également.
Troisièmement, comme certaines opérations d'E/S sont plus faciles, vous remarquerez peut-être que le déchargement du travail gourmand en ressources processeur vers un autre service ou une machine est beaucoup plus facile, ce qui peut également améliorer vos performances perçues (temps "d'horloge murale" plus court), mais dans l'ensemble la consommation de ressources augmentera: ajout d'une autre machine, temps passé sur les opérations réseau, etc.
Quatrième: UI. Vous ne voulez vraiment pas le geler. Il est très facile d'envelopper les tâches liées aux E/S et liées au processeur dans les tâches et asynchroniser/attendre sur elles et de garder l'interface utilisateur réactive. C'est pourquoi vous le voyez mentionné partout. Cependant, alors que les opérations liées aux E/S devraient idéalement être asynchrones jusqu'aux feuilles pour supprimer autant de temps d'attente inactif sur toutes les longues E/S, les tâches liées au CPU n'ont pas besoin d'être divisées ou asynchronisées plus de 1 descendre d'un niveau. Avoir un énorme travail de calcul monolithique enveloppé dans une seule tâche est juste suffisant pour débloquer l'interface utilisateur. Bien sûr, si vous avez plusieurs processeurs/cœurs, cela vaut toujours la peine de paralléliser tout ce qui est possible à l'intérieur, mais contrairement aux E/S - divisez trop et vous serez occupé à changer de tâche au lieu de mâcher les calculs.
En résumé: si vous avez des E/S qui prennent du temps - les opérations asynchrones peuvent gagner beaucoup de temps. Il est difficile d'exagérer les opérations d'E/S asynchronisées. Si vous avez des opérations prenant du CPU, ajouter quoi que ce soit consommera plus de temps CPU et plus de mémoire au total, mais le temps d'horloge murale peut être meilleur grâce à la division du travail en parties plus petites qui peuvent peut-être être exécutées sur plus de cœurs en même temps temps. Il n'est pas difficile d'en faire trop, vous devez donc être un peu prudent.
Le plus souvent, vous ne gagnez pas en performances directes (la tâche que vous effectuez se produit plus rapidement et/ou en moins de mémoire) comme en évolutivité; utiliser moins de threads pour effectuer le même nombre de tâches simultanées signifie que le nombre de tâches simultanées que vous pouvez effectuer est plus élevé.
Par conséquent, pour la plupart, vous ne trouvez pas qu'une opération donnée améliore les performances, mais peut trouver qu'une utilisation intensive a des performances améliorées.
Si une opération nécessite des tâches parallèles qui impliquent quelque chose de vraiment asynchrone (plusieurs E/S asynchrones), cette évolutivité peut cependant bénéficier à cette opération unique. Étant donné que le degré de blocage qui se produit dans les threads est réduit, cela se produit même si vous n'avez qu'un seul cœur, car la machine ne partage son temps qu'entre les tâches qui n'attendent pas actuellement.
Cela diffère des opérations parallèles liées au processeur qui (qu'elles soient effectuées à l'aide de tâches ou autrement) n'augmentent généralement que jusqu'au nombre de cœurs disponibles. (Les cœurs hyper-filetés se comportent comme 2 cœurs ou plus à certains égards et pas à d'autres).
La méthode s'exécute sur le contexte de synchronisation actuel et utilise l'heure sur le thread uniquement lorsque la méthode est active. Vous pouvez utiliser Task.Run pour déplacer le travail lié au processeur vers un thread d'arrière-plan, mais un thread d'arrière-plan n'aide pas avec un processus qui attend simplement que les résultats soient disponibles.
Lorsque vous avez un CPU et plusieurs threads dans votre application, votre CPU bascule entre les threads pour simuler un traitement parallèle. Avec async/attendent votre opération async n'a pas besoin de temps de thread, vous donnez donc plus de temps aux autres threads de votre application pour faire le travail. Par exemple, votre application (non-UI) peut toujours faire des appels HTTP, et tout ce dont vous avez besoin est d'attendre la réponse. C'est l'un des cas où l'avantage d'utiliser async/wait est important.
Lorsque vous appelez async DoJobAsync()
n'oubliez pas de .ConfigureAwait(false)
pour obtenir de meilleures performances pour les applications non UI qui n'ont pas besoin de fusionner avec le contexte du thread UI.
Je ne mentionne pas la syntaxe Nice qui aide beaucoup à garder votre code propre.
Les mots clés asynchrones et en attente n'entraînent pas la création de threads supplémentaires. Les méthodes asynchrones ne nécessitent pas de multithreading car une méthode asynchrone ne s'exécute pas sur son propre thread. La méthode s'exécute sur le contexte de synchronisation actuel et utilise l'heure sur le thread uniquement lorsque la méthode est active. Vous pouvez utiliser Task.Run pour déplacer le travail lié au processeur vers un thread d'arrière-plan, mais un thread d'arrière-plan n'aide pas avec un processus qui attend simplement que les résultats soient disponibles.
La fonctionnalité d'attente asynchrone de .NET n'est pas différente de celle des autres frameworks. Il ne donne pas un avantage de performance dans les calculs locaux, mais il permet simplement de basculer en continu entre les tâches dans un seul thread au lieu de laisser une tâche bloquer le thread. Si vous souhaitez un gain de performances pour les calculs locaux, utilisez la bibliothèque parallèle de tâches.
Visitez https://msdn.Microsoft.com/en-us/library/dd460717 (v = vs.110) .aspx