Pourquoi toutes les fonctions ne devraient-elles pas être asynchrones par défaut?

Le modèle async-waiting de .net 4.5 change de paradigme. C’est presque trop beau pour être vrai.

J’ai porté un code lourd d’IO sur async-waiting parce que le blocage est une chose du passé.

Beaucoup de gens comparent l’attente async à une infestation de zombies et j’ai trouvé que c’était assez précis. Le code async aime les autres codes asynchrones (vous avez besoin d’une fonction asynchrone pour pouvoir attendre une fonction asynchrone). 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 peu imaginatif. Lancez un mot-clé async dans la déclaration, encapsulez la valeur Task par Task et vous avez terminé. Il est plutôt déconcertant de voir à quel point le processus est facile à réaliser, et 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 simplement le rendre tout asynchrone par défaut?

La raison évidente que je suppose est la performance. Async-wait a un overhead et un code qui n’a pas besoin d’être asynchrone, de préférence pas. Mais si la performance est le seul problème, des optimisations intelligentes peuvent certainement supprimer automatiquement la surcharge lorsque cela n’est pas nécessaire. J’ai lu à propos de l’optimisation “fast path” , et il me semble que seule elle devrait en prendre en charge.

Peut-être est-ce comparable au changement de paradigme apporté par les éboueurs. Au début des jours GC, libérer sa propre mémoire était définitivement plus efficace. Mais les masses ont toujours opté pour 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 asynchronisées?

    Tout d’abord, merci pour vos bons mots. C’est en effet une fonctionnalité géniale et je suis heureux d’en avoir été une petite partie.

    Si tout mon code tourne lentement en asynchrone, pourquoi ne pas le rendre tout simplement asynchrone par défaut?

    Eh bien, tu exagères; tout votre code ne devient pas asynchrone. Lorsque vous ajoutez deux entiers “simples”, vous n’attendez pas le résultat. Lorsque vous ajoutez deux entiers futurs ensemble pour obtenir un troisième nombre entier – parce que c’est ce que la Task est, c’est un entier auquel vous aurez access à l’avenir – bien sûr, vous attendez probablement le résultat.

    La principale raison de ne pas tout rendre asynchrone est que le but de l’async / wait est de faciliter l’écriture de code dans un monde comportant de nombreuses opérations à forte latence . La grande majorité de vos opérations ne sont pas caractérisées par une forte latence. Par conséquent, cela n’a aucun sens de réduire l’impact de la latence sur les performances. Au lieu de cela, quelquesunes de vos opérations clés sont la latence élevée, et ces opérations provoquent l’infestation de l’async par des zombies dans tout le code.

    Si la performance est le seul problème, des optimisations intelligentes peuvent certainement supprimer automatiquement la surcharge lorsque cela 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’un passage d’optimisation.

    Le premier point est encore le suivant: l’async en C # / VB / F # est essentiellement une forme limitée de continuation de passage . Une énorme quantité de recherche dans la communauté des langages fonctionnels a permis de déterminer comment optimiser le code qui fait un usage intensif du style de transmission continue. L’équipe du compilateur devrait probablement résoudre des problèmes très similaires dans un monde où “async” était la méthode par défaut et 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.

    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 faciles à gérer. Par “transparence référentielle”, j’entends la propriété dont la valeur d’une expression ne dépend pas de son évaluation . Les expressions comme 2 + 2 sont transparentes par rapport à la référence; 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 avec le temps .

    Async rend beaucoup plus difficile de raisonner quand un effet secondaire se produira. Avant async, si vous avez dit:

     M(); N(); 

    et M() était void M() { Q(); R(); } void M() { Q(); R(); } void M() { Q(); R(); } , et N() était void N() { S(); T(); } void N() { S(); T(); } void N() { S(); T(); } , et R et S produisent des effets secondaires, 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(); } async void M() { await Q(); R(); } async void M() { await Q(); R(); } alors soudainement ça sort par la fenêtre. Vous n’avez aucune garantie que R() se produise avant ou après S() (à moins bien sûr que M() soit attendu, mais que sa Task ne doit pas être attendue avant N() .)

    Maintenant, imaginez que cette propriété de ne plus savoir quels effets secondaires de la commande se produisent s’applique à chaque morceau de code de votre programme, à l’ exception de ceux que l’optimiseur parvient à désynchroniser. En gros, 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 demander “pourquoi l’async est-il si spécial?” Si vous pensez que chaque opération devrait être une Task vous devez être capable de répondre à la question “Pourquoi ne pas Lazy ?” ou “pourquoi pas Nullable ?” ou “pourquoi pas IEnumerable ?” Parce que nous pourrions tout aussi facilement le faire. Pourquoi ne devrait-il pas arriver que chaque opération soit annulée ? 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, donc je peux générer un meilleur code”, et ainsi de suite. (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 la Task est en fait si spéciale pour justifier autant de travail.

    Si ce genre de choses vous intéresse, je vous recommande d’étudier les langages fonctionnels tels que Haskell, qui offrent une transparence référentielle beaucoup plus forte et permettent toutes sortes d’évaluation hors service et de mise en cache automatique. Haskell a également un soutien beaucoup plus fort dans son système de types pour les sortes de “soulèvements monadiques” auxquels j’ai fait allusion.

    Pourquoi toutes les fonctions ne devraient-elles pas être asynchrones?

    La performance est une des raisons, comme vous l’avez mentionné. Notez que l’option “fast path” à laquelle vous avez lié améliore les performances dans le cas d’une tâche terminée, mais elle nécessite encore beaucoup plus d’instructions et de surcharge par rapport à un appel de méthode unique. En tant que tel, même avec le “fast path” en place, vous ajoutez beaucoup de complexité et de surcharge à chaque appel de méthode asynchrone.

    La rétrocompatibilité, 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 méthode async rend la complexité de leur utilisation plus complexe. Cela est particulièrement vrai si vous n’avez pas de contexte de synchronisation, car les méthodes asynchrones peuvent alors finir facilement par provoquer des problèmes de threads inattendus.

    De plus, il existe de nombreuses routines qui, par nature, ne sont pas asynchrones. Ceux-ci ont plus de sens que les opérations synchrones. Forcer Math.Sqrt à être une Task Math.SqrtAsync serait ridicule, par exemple, car il n’ya aucune raison d’être asynchrone. Au lieu d’avoir recours à une application async , vous vous retrouveriez avec un système d’ await partout .

    Cela briserait aussi complètement le paradigme actuel et causerait des problèmes avec les propriétés (qui ne sont en fait que des paires de méthodes… deviendraient-elles aussi asynchrones?) Et aurait d’autres répercussions dans la conception du framework et du langage.

    Si vous faites beaucoup de travail lié aux E / S, vous aurez tendance à constater que l’utilisation async systématique est un excellent ajout, bon nombre de vos routines seront async . Cependant, lorsque vous commencez à faire un travail lié au processeur, en général, rendre les choses async n’est pas bon – cela cache le fait que vous utilisez des cycles de processeur sous une API qui semble asynchrone, mais qui n’est pas vraiment asynchrone.

    Mis à part la performance – async peut avoir un coût de productivité. Sur le client (WinForms, WPF, Windows Phone), c’est un avantage pour la productivité. Mais sur le serveur, ou dans d’autres scénarios non-interface utilisateur, vous payez de la productivité. Vous ne voulez certainement pas y aller par défaut en asynchrone. Utilisez-le lorsque vous avez besoin des avantages d’évolutivité.

    Utilisez-le à l’endroit idéal. 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 à l’extensibilité. Méthodes de sélection sélective async ne fonctionne que si votre code n’évolue jamais et que vous savez que la méthode A () est toujours liée au processeur (vous la conservez synchronisée) et que la méthode B () est toujours liée (vous la marquez comme asynchrone).

    Mais si les choses changent? Oui, A () effectue des calculs mais, à un moment donné, vous deviez append la journalisation ou la génération de rapports, ou un rappel défini par l’utilisateur avec une implémentation impossible à prédire, ou bien l’algorithme a été étendu aussi des entrées / sorties? Vous aurez besoin de convertir la méthode en asynchrone, mais cela briserait l’API et tous les appelants de la stack auraient également besoin d’être mis à jour (et ils pourraient même être des applications différentes provenant de fournisseurs différents). Ou vous devrez append 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 guère acceptable.

    Ce serait bien s’il était possible de rendre la méthode de synchronisation existante asynchrone sans modifier l’API. Mais dans la réalité, je pense que nous n’avons pas cette option et que l’utilisation de la version asynchrone, même si elle n’est pas nécessaire actuellement, est le seul moyen de garantir que vous ne rencontrerez jamais de problèmes de compatibilité à l’avenir.