Le modèle async-wait de .net 4.5 change de paradigme. C'est presque trop beau pour être vrai.
J'ai porté du code lourd d'ES vers async-wait car le blocage appartient au passé.
Un bon nombre de personnes comparent l'asynchronisation à une infestation de zombies et j'ai trouvé que c'était assez précis. Le code asynchrone aime les autres codes asynchrones (vous avez besoin d'une fonction asynchrone pour attendre une fonction asynchrone). Ainsi, de plus en plus de fonctions deviennent asynchrones et cela continue de croître dans votre base de code.
Changer les fonctions en async est un travail quelque peu répétitif et sans imagination. Lancez un mot clé async
dans la déclaration, encapsulez la valeur de retour par Task<>
et vous avez à peu près terminé. C'est assez troublant de voir à quel point tout le processus est facile, et très bientôt un script de remplacement de texte automatisera la plupart des "portages" pour moi.
Et maintenant la question .. Si tout mon code tourne lentement asynchrone, pourquoi ne pas tout faire asynchrone par défaut?
La raison évidente que je suppose est la performance. Async-wait a un surdébit 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 les frais généraux lorsqu'ils ne sont pas nécessaires. J'ai lu au sujet de l'optimisation "fast path" , et il me semble que c'est à lui seul qu'il devrait s'occuper de la plupart.
Peut-être que cela est comparable au changement de paradigme provoqué par les éboueurs. Au début du GC, libérer votre propre mémoire était certainement plus efficace. Mais les masses ont toujours choisi la collecte automatique en faveur d'un code plus sûr et 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?
Tout d'abord, merci pour vos aimables paroles. C'est en effet une fonctionnalité impressionnante et je suis heureux d'en avoir été une petite partie.
Si tout mon code devient lentement asynchrone, pourquoi ne pas tout faire asynchrone par défaut?
Eh bien, vous exagérez; tous 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 - - parce que c'est ce que Task<int>
est, c'est un entier auquel vous aurez accès à l'avenir - bien sûr, vous attendez probablement le résultat.
La principale raison de ne pas tout faire asynchrone est parce que le but de l'async/wait 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 à latence élevée, il n’a donc aucun sens de profiter des performances qui atténuent cette latence. Au contraire, une clé quelques-unes de vos opérations sont à latence élevée, et ces opérations provoquent l'infestation zombie d'async dans tout le code.
si les performances sont le seul problème, certaines optimisations intelligentes peuvent certainement supprimer automatiquement la surcharge lorsqu'elle n'est pas nécessaire.
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 à nouveau: async en C #/VB/F # est essentiellement une forme limitée de passage continu . Une énorme quantité de recherches dans la communauté des langages fonctionnels a été consacrée à trouver des moyens d'identifier la façon d'optimiser le code qui fait un usage intensif du 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 les méthodes non asynchrones devaient être identifiées et désasynchronisé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.
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 maniables. Par "transparence référentielle", j'entends la propriété dont la valeur d'une expression ne dépend pas du moment où 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 jusqu'à l'exécution et obtenir la même réponse. Mais une expression comme x+y
Ne peut pas être déplacée dans le temps car x et y peuvent changer au fil du temps .
Async, il est beaucoup plus difficile de savoir quand un effet secondaire se produira. Avant l'async, si vous avez dit:
M();
N();
et M()
était void M() { Q(); R(); }
et N()
était void N() { S(); T(); }
et R
et S
produire côté effets, alors vous savez que l'effet secondaire de R se produit avant l'effet secondaire de S. Mais si vous avez async void M() { await Q(); R(); }
alors soudainement cela sort par la fenêtre. Vous n'avez aucune garantie si R()
va se produire avant ou après S()
(sauf si bien sûr M()
est attendu; mais bien sûr son Task
n'a pas besoin d'être attendu avant N()
.)
Imaginez maintenant que cette propriété de ne sachant plus dans quel ordre se produisent les effets secondaires s'applique à chaque morceau de code de votre programme sauf ceux que l'optimiseur parvient à désynchroniser-ify . Fondamentalement, vous ne savez plus quelles expressions 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 vous demander "pourquoi l'async est-il si spécial?" Si vous prétendez que chaque opération doit en fait être un Task<T>
, Vous devez être en mesure de répondre à la question "pourquoi pas Lazy<T>
?" ou "pourquoi pas Nullable<T>
?" ou "pourquoi pas IEnumerable<T>
?" Parce que nous pourrions tout aussi facilement le faire. 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 ensuite essayer d'optimiser les situations où vous savez "oh, cela ne doit jamais être nul, afin que je puisse générer un meilleur code", etc. (Et en fait, le compilateur C # le fait pour l'arithmétique levée.)
Le point étant: il n'est pas clair pour moi que Task<T>
Est en fait si spécial pour justifier autant de travail.
Si ce genre de choses vous intéresse, je vous recommande d'enquêter sur les langages fonctionnels comme Haskell, qui ont une transparence référentielle beaucoup plus forte et permettent toutes sortes d'évaluation hors service et effectuent une mise en cache automatique. Haskell a également un support beaucoup plus fort dans son système de type pour les sortes de "levées monadiques" auxquelles j'ai fait allusion.
Pourquoi toutes les fonctions ne devraient-elles pas être asynchrones?
La performance est l'une des raisons, comme vous l'avez mentionné. Notez que l'option "chemin rapide" à laquelle vous avez lié améliore les performances dans le cas d'une tâche terminée, mais elle nécessite toujours 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 langues (y compris les scénarios d'interopérabilité), deviendrait également problématique.
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 cachent 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 alors facilement provoquer des problèmes de threads inattendus.
De plus, il existe de nombreuses routines qui ne sont pas, par nature, asynchrones. Celles-ci ont plus de sens que les opérations synchrones. Forcer Math.Sqrt
être Task<double> Math.SqrtAsync
serait ridicule, par exemple, car il n'y a aucune raison pour que cela soit asynchrone. Au lieu d'avoir async
Pousser à travers votre application, vous vous retrouveriez avec await
propager partout.
Cela briserait également complètement le paradigme actuel et entraînerait des problèmes avec les propriétés (qui ne sont en fait que des paires de méthodes ... iraient-ils aussi en mode asynchrone?), Et aurait d'autres répercussions tout au long de la conception du cadre et du langage.
Si vous faites beaucoup de travail IO lié, vous aurez tendance à trouver que l'utilisation de async
de manière omniprésente est un excellent ajout, un grand nombre de vos routines seront async
. Cependant, lorsque vous commencez à faire du travail lié au CPU, en général, faire des choses async
n'est en fait pas bon - cela cache le fait que vous utilisez des cycles CPU sous une API qui apparaît pour être asynchrone, mais n'est pas nécessairement vraiment vraiment asynchrone.
Mis à part les performances - 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-UI, vous payez la productivité. Vous ne voulez certainement pas y aller asynchrone par défaut. Utilisez-le lorsque vous avez besoin des avantages d'évolutivité.
Utilisez-le lorsque vous êtes au bon endroit. Dans d'autres cas, non.
Je crois qu'il y a une bonne raison de rendre toutes les méthodes asynchrones si elles ne sont pas nécessaires pour l'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 CPU (vous la gardez synchronisée) et la méthode B() est toujours lié aux E/S (vous le marquez comme async).
Et si les choses changent? Oui, A() fait des calculs mais à un moment donné dans le futur, vous avez dû y ajouter la journalisation, ou le rapport, 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 désormais non seulement des calculs CPU mais aussi des E/S? Vous devrez convertir la méthode en async, mais cela casserait l'API et tous les appelants de la pile devraient également être mis à jour (et ils peuvent même être différentes applications de différents fournisseurs.) Ou vous devrez ajouter une version asynchrone à côté de la version de synchronisation, mais cela ne fait pas beaucoup de différence - l'utilisation de la version de synchronisation bloquerait et n'est donc pas 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.