Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Task, qui es-tu ? partie 12 Les patterns de l’Asynchrone

Après l’étude de Task en 11 parties qui précèdent faisons un point sur les différentes approches de l'asynchronisme pour conclure. Task y joue un rôle important mais aussi async/await tout comme la bonne compréhension de l’asynchronisme lui-même…

Liens rapides vers la série complète

L’asynchronisme

La programmation asynchrone est souvent mal comprise car dès le départ le terme n’est pas forcément parlant (on comprend mais on ne “visualise” pas ce que c’est) et il arrive dans un flot incroyable de librairies ou modifications du langage C# qui semblent toutes tourner autour du même problème. Le parallélisme par exemple est une de ces notions qui vient parasiter en quelque sorte la compréhension de l’asynchronisme.

L’objectif

Posons alors le but de l’asynchronisme : Cela permet de rendre les applications “responsive” (réactives), c’est à dire non bloquantes et donnant au moins l’illusion à l’utilisateur que rien n’est bloqué ou figé.

Le moyen

Sur quoi se base l’asynchronisme pour atteindre ce but ? … Sur l’utilisation habile du temps d’attente des opérations longues qui ne sont pas réalisées par le code de l’application.

En effet, de nombreuses opérations dépendent au moins de l’OS lui-même. Lire un fichier, l’écrire sont des choses qui prennent un “certain temps” mais qui bloquent le programme jusqu’à ce qu’elles soient terminées. On pense aussi à toutes les connexions avec “l’extérieur” qui se font via des réseaux locaux ou pire via internet dont la disponibilité, la bande passante, la réactivité sont aléatoires.

En programmation classique accéder à de telles ressources bloque le code en cours car il attend la fin de l’opération pour continuer. Si j’ai besoin de lire un fichier de configuration, inutile que je passe à la ligne suivante de mon programme qui a besoin des valeurs lues tant que tout le fichier n’est pas lu.

Cela semble tellement évident ce besoin de séquentiel qu’on voit mal comment on pourrait faire autrement. Il n’est pas possible de voyager dans le temps ni d’utiliser un résultat avant qu’il ne soit disponible…

C’est vrai. Mais on peut faire autre chose en attendant… Lire un autre fichier, transmettre des informations via internet sur l’état du programme, envoyer un mail automatique à un administrateur ou milles et une choses qui n’ont pas besoin du résultat de la première lecture évoquée plus haut et qui elles-mêmes peuvent induire des attentes de ressources externes. Pourquoi attendre, pourquoi bloquer le programme alors que toutes ces attentes sont “extérieures” et que pendant celles-ci aucun code de l’application n’est utilisé ? En libérant au moins le thread principal qui gère l’UI dans les applications modernes on redonne de la réactivé à l’application plutôt que de la voir rester figée, et rien que cela change tout dans la perception que l’utilisateur a de l’environnement, de l’application et de l’OS.

Comment ?

Il y a plusieurs façons d’arriver au but. Certaines opérations peuvent tout simplement être réalisées dans des threads distincts, c’est ce à quoi sert Task le plus souvent. D’autres ne sont qu’attente par essence, il faut donc mettre en place un moyen de faire autre chose pendant cette attente (sans qu’aucun code ne soit exécuté par cette attente), c’est le cas avec Task aussi quand on créée des Promise Tasks au lieu de Delegate Tasks (avec code exécuté).

On peut aussi rendre une application plus réactive en utilisant le parallélisme qui lui s’attache à utiliser au mieux les multiples cœurs des machines modernes. Pour cela on utilise la classe Parallel ou PLINQ par exemple. Le plus souvent il s’agit de traiter des données et en découpant le travail sur plusieurs processeurs on accélère grandement l’application la rendant plus agile, plus fluide et plus réactive aussi. Mais le parallélisme ne s’intéresse qu’à cela, traiter des données sur plusieurs cœurs. C’est son seul but. Si parmi les avantages on retrouve la réactivité globale de l’application ce n’est que par effet de bord, tout bêtement parce que l’application travaille plus vite donc semble plus performante. C’est là qu’il faut comprendre la nuance entre parallélisme et asynchronisme et on lira à ce sujet mon article

 

Aucune de ces deux approches n’est “la meilleure”, au contraire, elles se complètent et doivent être utilisées en même temps pour converger vers le but final d’une application fluide et réactive.

Si la performance brute d’une application peut être améliorée par le parallélisme, l’asynchronisme n’a pas d’autre but que la réactivité. Et d’ailleurs le premier implique l’exécution simultanée de code sur plusieurs cœurs alors que le second n’implique en aucun cas de faire du multitâche ! Là aussi il faut comprendre cette nuance de taille (que j’ai déjà abordé plusieurs fois il est vrai, donc ça devrait commencer à rentrer !).

Bref, il existe de nombreuses façons d’atteindre le but, l’asynchronisme qui nous intéresse dans cet article fait partie de la panoplie en citoyen de première classe. Car la réactivité est avant tout affaire d’asynchronisme plus que de multitâche. Et c’est vrai, quoi qu’on fasse, multitâche et asynchronisme finissent toujours pas se rencontrer quelque part, entretenant une confusion que seule la pratique de ces deux aspects permet de lever…

Les trois patterns de l’asynchronisme

Il y en existe bien plus mais en tout cas le framework .NET nous en offre trois :

  • APM (Asynchronous Programming Model – Modèle de Programmation Asynchrone) qui est aussi appelé pattern IAsyncResult du nom de l’interface qui joue un rôle central dans cette approche. Ce modèle est basé sur un découpage des méthodes longues en deux opérations, l’une préfixée par “Begin” pour lancer la tâche, l’autre par “End” pour récupérer le résultat de celle-ci (par exemple BeginWrite et EndWrite). Il s’agit de la première tentative de Microsoft de rationnaliser l’asynchronisme sous .NET. Ce modèle était particulièrement pénible dès que plusieurs opérations devaient s’enchaîner ou dépendaient les unes des autres. Silverlight avec les RIA Services a donné l’occasion de s’apercevoir à quel point cette approche avait ces limites ! Bien que toujours présente dans le framework pour gérer la compatibilité des anciennes applications APM n’est plus utilisé et ne doit plus l’être.
  • EAP (Event-bases Asynchronous Pattern – Pattern Asynchrone basé sur les Evènements) utilise une autre approche, celles de méthodes qui ont le suffixe “Async” et met en oeuvre des stratégies basées sur un ou plusieurs évènements, des types de delegate et des types dérivés de EventArgs. Cette approche a été introduite dans .NET 2.0 et là encore elle a montré ses limites et n’est plus utilisée.
  • TAP (Task-based Asynchronous Pattern – Pattern basé sur les Tâches) utilise une seule méthode pour gérer le lancement d’une opération et la récupération de son éventuel résultat le tout de façon asynchrone. TAP est une nouveauté de .NET 4.0 et c’est aujourd’hui l’approche recommandée. Les ajouts tels que async/await font partie des améliorations de TAP pour rendre le pattern encore plus simple à mettre œuvre.

 

Pour bien comprendre TAP rien de mieux que de comprendre l’évolution APM/EAP/TAP.

Ici aussi je renvoie le lecteur à l’un de mes articles :

 

Les autres approches

D’autres approches ont été tentées, notamment par le biais de librairies tierces, des frameworks MVVM, etc…

En 2011, ce qui ne nous rajeunit pas mais qui prouve à quel point Dot.Blog vous donne toujours une information qui a de l’avance (pub gratos pour moi-même !) j’avais présenté différents procédés dans l’article suivant :

 

Si Silverlight n’est plus d’actualité, la définition de l’Asynchronisme, des co-routines, la présentation des méthodes proposées par des frameworks comme Jounce sont autant de vérités intemporelles sur la problématique de l’asynchronisme et la lecture de cet article vous permettra certainement de mieux aborder encore cette dernière en puisant aux racines de sa mise en œuvre sous .NET.

Dans la même veine et d’une même époque, j’avais aussi écrit l’article suivant :

 

Ici aussi il s’agissait de répondre à ce besoin d’asynchronisme par différentes approches. Le plus intéressant avec le recul est la façon dont je montre comment arrivée à “penser autrement” pour utiliser l’asynchronisme avec le pattern MVVM. C’est un sujet que j’aborderais bientôt dans le contexte ultra moderne de Universal App Platform, la relecture de cet ancien article permettra certainement de donner du relief à la problématique…

Conclusion

imageJ’avais utilisé dans la première partie la tête du Terminator mi-humaine mi-robot pour illustrer les deux faces d’une tâche, dualité un peu inquiétante. Arrivés à la fin de cette série nous pouvons donner un peu le sourire à notre ami Schwarzy, Task ne vous fait plus peur désormais !

Cette douzième partie était surtout l’occasion de replacer Task dans une longue progression vers une modèle de programmation ultra simplifié et efficace pour mieux le comprendre. C’était aussi un moyen de pointer quelques articles qui depuis des années jalonnent Dot.Blog en distillant à chaque fois un peu plus de vérité sur l’asynchronisme. Les relire renforce certainement la compréhension de la problématique posée, les différentes approches pour arriver au modèle TAP éclairant mieux sa raison d’être. Et la chance que nous avons d’utiliser un framework et un langage qui savent évoluer !

Stay Tuned !

blog comments powered by Disqus