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.
Si tout mon code devient lentement asynchrone, pourquoi ne pas simplement le rendre asynchrone par défaut?
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.
si les performances sont le seul problème, certaines optimisations intelligentes peuvent certainement supprimer la surcharge automatiquement 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 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 comme x+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:
M();
N();
et M()
était void M() { Q(); R(); }
, et N()
était void N() { S(); T(); }
, et R
et S
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 cela R()
se produise avant ou après S()
(à moins que bien sûr ne M()
soit attendu; mais bien sûr, il Task
n'est pas nécessaire d'attendre qu'après N()
.)
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 pas Lazy<T>
?" ou "pourquoi pas Nullable<T>
?" ou "pourquoi pas IEnumerable<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.