Le modèle async-await de .net 4.5 est en train de changer de paradigme. C'est presque trop beau pour être vrai.
J'ai porté du code lourd d'E / S sur async-await car le blocage est une chose du passé.
De nombreuses personnes comparent l'async-await à une infestation de zombies et j'ai trouvé que c'était plutôt précis. Le code async aime les autres codes async (vous avez besoin d'une fonction async pour attendre une fonction async). Ainsi, de plus en plus de fonctions deviennent asynchrones et cela ne cesse de croître dans votre base de code.
Changer les fonctions en asynchrone est un travail quelque peu répétitif et sans imagination. Jetez un async
mot-clé dans la déclaration, encapsulez la valeur de retour Task<>
et vous avez presque terminé. La facilité de tout le processus est assez troublante, et très bientôt un script de remplacement de texte automatisera la plupart du «portage» pour moi.
Et maintenant la question ... Si tout mon code devient lentement asynchrone, pourquoi ne pas simplement le rendre asynchrone par défaut?
La raison évidente que je suppose est la performance. Async-await a une surcharge et un code qui n'a pas besoin d'être asynchrone, de préférence pas. Mais si les performances sont le seul problème, certaines optimisations intelligentes peuvent certainement supprimer automatiquement la surcharge lorsqu'elle n'est pas nécessaire. J'ai lu sur l' optimisation du "chemin rapide" , et il me semble qu'elle devrait à elle seule s'en occuper en grande partie.
C'est peut-être comparable au changement de paradigme apporté par les éboueurs. Au début du GC, libérer sa propre mémoire était nettement plus efficace. Mais les masses ont toujours choisi la collecte automatique en faveur d'un code plus sûr, plus simple qui pourrait être moins efficace (et même cela n'est sans doute plus vrai). Peut-être que cela devrait être le cas ici? Pourquoi toutes les fonctions ne devraient-elles pas être asynchrones?
la source
async
(pour remplir un contrat), c'est probablement une mauvaise idée. Vous obtenez les inconvénients de l'async (coût accru des appels de méthode; la nécessité de l'utiliserawait
dans le code d'appel), mais aucun des avantages.Réponses:
Tout d'abord, merci pour vos aimables paroles. C'est en effet une fonctionnalité géniale et je suis heureux d'en avoir été une petite partie.
Eh bien, vous exagérez; tout votre code ne devient pas asynchrone. Lorsque vous ajoutez deux entiers "simples" ensemble, vous n'attendez pas le résultat. Lorsque vous ajoutez deux futurs entiers ensemble pour obtenir un troisième futur entier - car c'est ce qui
Task<int>
est, c'est un entier auquel vous allez avoir accès dans le futur - bien sûr, vous attendez probablement le résultat.La raison principale pour ne pas rendre tout asynchrone est que le but de async / await est de faciliter l'écriture de code dans un monde avec de nombreuses opérations à latence élevée . La grande majorité de vos opérations ne sont pas à forte latence, il n'est donc pas logique de prendre le coup de performance qui atténue cette latence. Au contraire, quelques - unes de vos opérations clés sont à forte latence, et ces opérations sont à l'origine de l'infestation de zombies par async dans tout le code.
En théorie, la théorie et la pratique sont similaires. En pratique, ils ne le sont jamais.
Permettez-moi de vous donner trois points contre ce type de transformation suivi d'une passe d'optimisation.
Le premier point est encore: async en C # / VB / F # est essentiellement une forme limitée de passage de continuation . Une énorme quantité de recherche dans la communauté des langages fonctionnels a été consacrée à trouver des moyens d'identifier comment optimiser le code qui utilise beaucoup le style de passage de continuation. L'équipe du compilateur devrait probablement résoudre des problèmes très similaires dans un monde où "async" était la valeur par défaut et où les méthodes non asynchrones devaient être identifiées et désynchronisées. L'équipe C # n'est pas vraiment intéressée à s'attaquer à des problèmes de recherche ouverts, c'est donc de gros points contre là.
Un deuxième point contre est que C # n'a pas le niveau de «transparence référentielle» qui rend ces sortes d'optimisations plus traitables. Par «transparence référentielle», j'entends la propriété dont la valeur d'une expression ne dépend pas lorsqu'elle est évaluée . Les expressions comme
2 + 2
sont référentiellement transparentes; vous pouvez faire l'évaluation au moment de la compilation si vous le souhaitez, ou la reporter à l'exécution et obtenir la même réponse. Mais une expression commex+y
ne peut pas être déplacée dans le temps car x et y peuvent changer avec le temps .Async rend beaucoup plus difficile de raisonner sur le moment où un effet secondaire se produira. Avant async, si vous avez dit:
et
M()
étaitvoid M() { Q(); R(); }
, etN()
étaitvoid N() { S(); T(); }
, etR
etS
produisait des effets secondaires, alors vous savez que l'effet secondaire de R se produit avant l'effet secondaire de S. Mais si vous l'avez,async void M() { await Q(); R(); }
tout à coup, cela sort par la fenêtre. Vous n'avez aucune garantie que celaR()
se produise avant ou aprèsS()
(à moins que bien sûr neM()
soit attendu; mais bien sûr, ilTask
n'est pas nécessaire d'attendre qu'aprèsN()
.)Imaginez maintenant que cette propriété de ne plus savoir dans quel ordre les effets secondaires se produisent s'applique à chaque morceau de code de votre programme, à l' exception de ceux que l'optimiseur parvient à désynchroniser. En gros, vous n'avez plus la moindre idée des expressions qui seront évaluées dans quel ordre, ce qui signifie que toutes les expressions doivent être référentiellement transparentes, ce qui est difficile dans un langage comme C #.
Un troisième point contre est que vous devez alors demander "pourquoi l'async est-il si spécial?" Si vous prétendez que chaque opération devrait en fait être une opération,
Task<T>
vous devez être en mesure de répondre à la question "pourquoi pasLazy<T>
?" ou "pourquoi pasNullable<T>
?" ou "pourquoi pasIEnumerable<T>
?" Parce que nous pourrions tout aussi bien faire cela. Pourquoi ne devrait-il pas être le cas que chaque opération est levée à nullable ? Ou chaque opération est calculée paresseusement et le résultat est mis en cache pour plus tard , ou le résultat de chaque opération est une séquence de valeurs au lieu d'une seule valeur . Vous devez alors essayer d'optimiser les situations où vous savez "oh, cela ne doit jamais être nul, pour que je puisse générer un meilleur code", et ainsi de suite.Le fait est que ce n'est pas clair pour moi que ce
Task<T>
soit vraiment si spécial pour justifier autant de travail.Si ce genre de choses vous intéresse, je vous recommande d'étudier les langages fonctionnels comme Haskell, qui ont une transparence référentielle beaucoup plus forte et permettent toutes sortes d'évaluation dans le désordre et font une mise en cache automatique. Haskell a également un soutien beaucoup plus fort dans son système de type pour les types de "levées monadiques" auxquelles j'ai fait allusion.
la source
async/await
parce que quelque chose de caché sous des couches d'abstraction peut être asynchrone.La performance est l'une des raisons, comme vous l'avez mentionné. Notez que l'option "chemin rapide" que vous avez associée améliore les performances dans le cas d'une tâche terminée, mais elle nécessite encore beaucoup plus d'instructions et de temps système par rapport à un appel de méthode unique. En tant que tel, même avec le «chemin rapide» en place, vous ajoutez beaucoup de complexité et de surcharge à chaque appel de méthode asynchrone.
La compatibilité descendante, ainsi que la compatibilité avec d'autres langages (y compris les scénarios d'interopérabilité), deviendraient également problématiques.
L'autre est une question de complexité et d'intention. Les opérations asynchrones ajoutent de la complexité - dans de nombreux cas, les fonctionnalités du langage masquent cela, mais il existe de nombreux cas où la création de méthodes
async
ajoute définitivement de la complexité à leur utilisation. Cela est particulièrement vrai si vous n'avez pas de contexte de synchronisation, car les méthodes asynchrones peuvent facilement finir par causer des problèmes de threading inattendus.De plus, il existe de nombreuses routines qui ne sont pas, par nature, asynchrones. Celles-ci ont plus de sens en tant qu'opérations synchrones. Forcer
Math.Sqrt
à êtreTask<double> Math.SqrtAsync
serait ridicule, par exemple, car il n'y a aucune raison pour que cela soit asynchrone. Au lieu deasync
faire passer votre application, vousawait
finirez par se propager partout .Cela briserait aussi complètement le paradigme actuel, ainsi que causerait des problèmes avec les propriétés (qui ne sont en fait que des paires de méthodes. Seraient-ils aussi asynchrones?), Et auraient d'autres répercussions tout au long de la conception du framework et du langage.
Si vous faites beaucoup de travail lié aux E / S, vous aurez tendance à trouver que l'utilisation
async
généralisée est un excellent ajout, un grand nombre de vos routines le serontasync
. Cependant, lorsque vous commencez à travailler avec le processeur, en général, faire les chosesasync
n'est en fait pas bon - cela cache le fait que vous utilisez des cycles de processeur sous une API qui semble être asynchrone, mais qui n'est pas nécessairement vraiment asynchrone.la source
await FooAsync()
plus simple queFoo()
? Et au lieu d'un petit effet domino de temps en temps, vous avez un énorme effet domino tout le temps et vous appelez cela une amélioration?Outre les performances, l'async peut avoir un coût de productivité. Sur le client (WinForms, WPF, Windows Phone) c'est une aubaine pour la productivité. Mais sur le serveur, ou dans d'autres scénarios non liés à l'interface utilisateur, vous payez de la productivité. Vous ne voulez certainement pas y aller asynchrone par défaut. Utilisez-le lorsque vous avez besoin des avantages de l'évolutivité.
Utilisez-le lorsque vous êtes au sweet spot. Dans d'autres cas, ne le faites pas.
la source
Je crois qu'il y a une bonne raison de rendre toutes les méthodes asynchrones si elles ne sont pas nécessaires - extensibilité. Les méthodes de création sélective async ne fonctionnent que si votre code n'évolue jamais et que vous savez que la méthode A () est toujours liée au processeur (vous la gardez synchronisée) et la méthode B () est toujours liée aux E / S (vous la marquez comme asynchrone).
Mais que faire si les choses changent? Oui, A () fait des calculs, mais à un moment donné dans le futur, vous avez dû y ajouter une journalisation, ou des rapports, ou un rappel défini par l'utilisateur avec une implémentation qui ne peut pas prédire, ou l'algorithme a été étendu et inclut maintenant non seulement les calculs du processeur, mais aussi des E / S? Vous devrez convertir la méthode en asynchrone, mais cela briserait l'API et tous les appelants de la pile devraient également être mis à jour (et ils peuvent même être des applications différentes de différents fournisseurs). Ou vous devrez ajouter une version asynchrone à côté de la version sync, mais cela ne fait pas beaucoup de différence - l'utilisation de la version sync bloquerait et n'est donc guère acceptable.
Ce serait formidable s'il était possible de rendre la méthode de synchronisation existante asynchrone sans changer l'API. Mais dans la réalité, nous n'avons pas une telle option, je crois, et utiliser la version asynchrone même si elle n'est pas actuellement nécessaire est le seul moyen de garantir que vous ne rencontrerez jamais de problèmes de compatibilité à l'avenir.
la source