Dot.Blog

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

TAP, APM, EAP : et vous, vous en êtes où ?

La programmation évolue, d’une part parce qu’il s’agit d’une science appliquée encore en pleine évolution, d’autre part parce que les machines évoluent aussi ainsi que les attentes des utilisateurs. D’exotique, la programmation parallèle est devenue un passage obligé. Les tâches remplacent l’asynchronisme simple qui lui-même a supplanté événementiel. Mais vous, vous en êtes où dans votre compréhension de toutes ces techniques ?

Le contexte

Posons d’abord  le décor car rien ne peut se comprendre hors du contexte qui a vu naitre toutes les approches évoquées plus haut…

Plusieurs modèles (Patterns) se sont sont succédés pour aider le développeur dans sa difficile tâche de codage d’applications toujours plus réactives. Car en dehors de calculs intensifs réservés à quelques applications bien particulières c’est la latence qui est devenue l’ennemi public N°1 de l’informaticien soucieux d’une bonne UX car l’utilisateur, de docile et écrasé par la technologie, en est devenu un simple consommateur

Or une secrétaire qui pleure lorsqu’on remplace sa machine à écrire mécanique par un ordinateur pour faire les factures (j’ai vécu ce genre de scènes au début des années 80…) est tétanisée devant la technologie quelle sacralise et qui l’effraye à la fois. L’autorité des “savants” qui font marcher la machine est immense. Comment remettre en question la parole de ces demi-dieux capables de comprendre et de faire fonctionner ces machines si complexes, si nouvelles, des machines qu’on peut ouvrir et ne rien voir, car rien ne bouge, tout est subtil et invisible mouvement de charges électriques, de spins, de magnétisme microscopique.

Mais ça c’était avant !!!

L’informaticien d’aujourd’hui n’est plus un être exceptionnel faisant un métier savant, c’est un ouvrier spécialisé comme les autres. Les ordinateurs ? On se fiche de savoir si c’est compliqué ou pas, après tout ça fait longtemps qu’on n’ouvre plus le capot de sa voiture tellement on s’est éloigné des moteurs qu’un connaisseur pouvait bricoler sur le bord d’une route… Seuls quelques kékés ouvrent encore le capot de leur voiture d’un air entendu pour tenter d’épater quelque proie un peu cruche… Les ingénieurs appellent la dépanneuse car ils savent qu’ils n’ont pas le banc test électronique pour faire quoi que ce soit d’utile…

De savant à grouillot de base, l’informaticien a entrainé dans sa chute l’informatique et son émanation la plus voyante, l’ordinateur. A moins que ce ne soit le contraire. Mais le résultat est le même : un ordinateur c’est une voiture. On ne complexe plus de ne pas savoir réparer sa voiture, on ne complexe plus de pas savoir comment tous les objets électroniques qui nous entourent fonctionnent, et l’ordinateur, sous toutes ses formes, n’échappe pas à la règle.

Et quand un objet devient un objet de consommation, quand d’opérateur formé l’utilisateur d’ordinateur devient un simple consommateur de produit technologique, tout bascule…

La secrétaire apeurée évoquée plus haut tentait bien parfois de se révolter en saisissant de fausses données pour accuser l’ordinateur de tous les maux et tenter de revenir à sa machine à écrire si rassurante (je l’ai vu faire aussi…), mais ce n’était que révoltes vaines, sous-terraines et finalement vouées à l’échec.

Le consommateur d’aujourd’hui est très différent, bien moins malléable, moins docile, moins impressionné… Il n’a pas besoin de fomenter des sabotages s’il n’est pas content. Non, il zappe, c’est tout… Il a le choix, des machines, des OS, des applications, des UI, des UX, des techniques d’interface (souris, pad, Kinect, Glass, écrans tactiles…). C’est lui le boss aujourd’hui.

Et comme tout produit de consommation, l’informatique doit se plier aux exigences du consommateur, ce bébé trop gâté qui jette ses jouets très vite s’ils ne le séduisent pas immédiatement

Les séries TV sont conçues pour que lorsqu’on zappe on tombe sur une scène “intéressante” en moins de quelques secondes, les pubs sont conçues pour le zapping avec un format ultra court, et les chaînes concurrentes se synchronisent pour les diffuser afin de contrer l’effet du zapping anti-pub … Aujourd’hui le “mobinaute” zappe les applications plus vite que Lucky Luke surprend son ombre lorsqu'il dégaine !

Parallélisme, Asynchronisme et programmation concurrente

Parallel, Asynchronous and Concurrent ProgrammingAvant d’introduire les diverses évolutions de l’asynchronisme il est nécessaire de poser les différences suivantes :

  • La programmation Synchrone ou séquentielle est le modèle ancestral de développement ne permettant pas de rendre certaines opérations non bloquante, à cause du langage, de la plateforme ou des API de l’OS. On pense souvent à tort que ce mode s’oppose au multi-threading alors qu’il n’en est rien. En programmation synchrone on peut généralement lancer plusieurs threads pour faire du parallélisme, mais les opérations d’E/S principalement restent bloquantes sans qu’on puisse l’éviter.
  • Le Parallélisme consiste à effectuer des traitement multiples simultanément. Ces traitements sont généralement “CPU intensive” et peuvent s’appuyer sur un jeu de données partagé ou bien sur des données distinctes. Le but principal du parallélisme et de tirer profit des ordinateurs modernes dont les CPU sont dotés de plusieurs cœurs qui ne peuvent être activés simultanément qu’en utilisant plusieurs threads.
  • La programmation concurrente définit un mode de programmation où différents processus au sens large interagissent entre eux par exemple au travers de structures de données partagées (en mémoire ou non). La synchronisation des partages et leur intégrité est le sujet principal de la programmation concurrente qui peut être mise en œuvre via de la programmation parallèle ou non.
  • L’Asynchronisme est un mode de programmation axé sur la façon d’éviter les latences et attentes induites principalement par les opérations d’E/S comme les accès aux disques, au réseau, etc. L’asynchronisme ne consiste pas seulement à libérer le thread principal responsable de la gestion de l’UI (et donc de la fluidité pour l’utilisateur) mais tout thread, quel qu’il soit. Une opération asynchrone peut être invoquée dans n’importe quel thread de l’application, l’appel ne sera tout simplement pas bloquant. C’est la définition même du mode asynchrone. Le modèle peut être étendu à tout code dont l’exécution est longue ou peut l’être même s’il ne s’agit pas d’E/S bien entendu.

 

Au début fut l’APM

Et le Grand Concepteur créa le modèle EAP. Une révolution.

Dans des environnements pilotés par l’utilisateur comme Windows, ce n’est plus le programme qui impose un menu du type “tapez 1 pour faire ceci, tapez 2 pour faire cela…” comme on le voyait sous CP/M par exemple, l’utilisateur peut cliquer où il veut, quand il veut. Le modèle n’est plus purement séquentiel, il devient évènementiel.

L’évènementiel consiste à être en capacité de répondre à tout moment à toute commande de l’utilisateur. L’envoie et la gestion de messages ou la gestion des évènements de langages comme C# permettent de le faire de façon très efficace, mais de façon synchrone à l’intérieur de la gestion de l’évènement. Ce qui signifie que pendant qu’une action est traitée l”ordinateur ne peut en traiter d’autres. C’est évènementiel mais bloquant.

Les premières versions de Windows ont introduit un mode dit “multitâche coopératif” pour tenter de palier ce problème. Il s’agissait pour chaque développeur d’introduire dans son code, là où il le voulait, une instruction spéciale qui permettait à Windows de reprendre la main et de la donner à une autre tâche éventuelle. Chaque application étant conçue de la sorte, le modèle était bien “coopératif”, les applications étaient obligées de “coopérer” pour que l’OS semble a peu près réactif.

Mais ce n’était pas assez  en réalité. La fameuse instruction (ou appel d’API) ralentissait l’application et chacun voulant être plus rapide que le concurrent - surtout à une époque où les PC étaient assez lents – il n’était pas rare que les développeurs soient plus tentés par les performances de leur application plutôt que par une coopération bien gentille avec les autres applications…

Le multitâche préemptif viendra mettre fin à cette guerre de la coopération. C’est l’OS qui donne et qui prend la main sans rien demander à l’application, toutes deviennent égales devant l’OS et toutes ont les mêmes chances sans dépendre du bon vouloir de leurs développeurs. Mais si l’OS lui-même n’était plus bloqué par les applications, le traitement des messages par ces dernières le restait, mais uniquement pour l’application concernée. On se rappelle tous de fenêtres restant bloquées et ne répondant plus à la souris sans pour autant que le reste soit concerné (gros avantage du mode préemptif d'ailleurs).

La gestion des évènements a été une trouvaille tellement efficace qu’elle reste aujourd’hui toujours autant utilisée. C’est donc tout naturellement vers ce modèle qu’on s’est tourné lorsqu’il a fallu géré de l’asynchronisme pour donner aux applications un peu de réactivité et de fluidité.

Pour rappel l’asynchronisme consiste à débuter une tâche sans généralement en connaitre la “date” de fin exacte tout en faisant autre chose pendant cette attente. Cette possibilité de “faire autre chose” confère à l’application une nouvelle souplesse : la réactivité. Le multitâche même préemptif de l’OS ne joue qu’entre les applications, pas au sein de celles-ci. Si une application est mal écrite et que son code est bloquant, tant pis pour elle ! Avec le modèle coopératif c’est tout l’OS qui était bloqué (toutes les applications en cours aussi donc), avec les OS préemptifs, c’est seulement l’application bloquante qui se trouve pénalisée, les autres continuent à marcher parfaitement. On ne peut donc plus accuser l’OS ou les “autres”, l’utilisateur peut voir qui est bloquant et qui ne l’est pas ! Et il déteste les applications qui se bloquent !

le Programming Model (APM)

L’introduction du modèle APM dans .NET 1.1 fut le début de l'histoire. Ce modèle de développement asynchrone utilise le pattern IAsyncResult qui n’est autre qu’une interface exposant l’état d’une opération asynchrone en cours et demandant l’implémentation de deux méthodes : BeginNomOpération et EndNomOpération.

Le Framework .NET a fait un usage intensif de ce modèle. On trouve par exemple dans la classe FileStream un BeaginRead et un EndRead pour lire un fichier de façon asynchrone et cette stratégie a été utilisée partout où une opération un peu longue était définie (accès Web ou réseau par exemple).Notre classe exemple s’écrira : 

public class MyClass
{
    public IAsyncResult BeginRead(
        byte [] buffer, int offset, int count, 
        AsyncCallback callback, object state);
    public int EndRead(IAsyncResult asyncResult);
}

L’opération BeginRead permet de lancer la séquence de lecture, elle retourne une instance de l’interface IAsyncResult. Les paramètres de BeginRead intègrent désormais un Callback. C’est lui qui sera appelé lorsque l’opération prendra fin.

Quand le Callback est appelé il invoque l’opération de fin qui s’assure que tout est bien terminé et que les ressources sont relâchées.

Ce modèle qui n'était qu'une première approche n’était finalement pas très satisfaisant. Ici aussi l’imbrication des appels et la gestion des exceptions finissaient par rendre le code très difficile à maintenir.

Fausse bonne idée ? C’est possible… Toutefois il faut un début à tout et il reste beaucoup de code à maintenir qui utilise ce pattern et il est donc essentiel de ne pas l’oublier totalement !

L’Event-based Asynchronous Pattern (EAP)

L’EAP est une amélioration de APM. Introduit dans .NET 2.0 c'est un pattern qui permet de concevoir du code asynchrone en se basant sur la gestion des événements du langage tout comme APM avec lequel il conserve une grande proximité.

Supposons une classe qui doit lire dans un buffer une certaine quantité de données, le mode le plus “rustique” donc synchrone donnera une déclaration de ce type :

public class MyClass
{
    public int Read(byte [] buffer, int offset, int count);
}

La classe expose une méthode Read (lecture) qui ne rend la main qu’une fois le travail effectué, peu importe combien de temps il prend. Ce qui permet à la méthode Read de retourner une valeur comme par exemple ici le nombre d’octets réellement lus.

C’est un code simple, mais bloquant, soit, dit de façon plus élégante : synchrone.

Avec EAP la classe sera définie plutôt de la façon suivante :

public class MyClass
{
    public void ReadAsync(byte [] buffer, int offset, int count);
    public event ReadCompletedEventHandler ReadCompleted;
}
 
public delegate void ReadCompletedEventHandler(
    object sender, ReadCompletedEventArgs eventArgs);
 
public class ReadCompletedEventArgs : AsyncCompletedEventArgs
{
    public int Result { get; }
}

 

C’est déjà plus sophistiqué. On comprend que la méthode ReadAsync va retourner le trait immédiatement. Il sera donc possible à l’application de “passer à autre chose”, cet “autre chose” pouvant tout simplement être de ne rien faire pour attendre les prochaines commandes de l’utilisateur et donc apparaitre fluide et réactive. Le thread principal étant dédié à la gestion de l’UI (donc des commandes l’utilisateur et des affichages).

Mais quand savoir que les données ont été lues ? Quand savoir quelle quantité de données a été lue ?

Le mode synchrone est d’une grande simplicité, et beaucoup de développeurs codent encore comme cela d’ailleurs… Tout est simple, séquentiel. Avec l’EAP le code est plus sophistiqué à écrire et à déboguer. La souplesse a un prix. Ici la classe MyClass expose un évènement ReadCompleted auquel le code qui utilise MyClass devra s’abonner pour être averti de la fin de la commande ReadAsync.

C’est donc dans le gestionnaire de cet évènement que l’application pourra prendre connaissance du nombre d’octets lus et prendre en charge le traitement des données qui se trouvent désormais dans le buffer…

Asynchrone mais un lourd à écrire et à maintenir. Car lorsque du code de ce type doit en plus gérer les exceptions et les appels imbriqués, l’enfer n’est pas bien loin !

Enfin le Task-based Asynchronous Pattern (TAP) naquit

Toutes ces tentatives pour tendre vers un mode de programmation asynchrone ont été autant de jalons le long d’une longue route pleine d’embuches. Plus nécessaire qu’agréable, les modèles étudiés jusqu’ici n’avaient rien pour donner le gout de l’asynchrone ! Puissants, remplissant leur rôle, EAP et APM n’étaient vraiment pas très pratique à coder et encore moins à maintenir.

Il fallait une évolution majeure pour que l’asynchrone sorte de l’exotisme et de l’élitisme, bien plus que les “astuces” proposées jusqu’à lors.

Que cela soit EAP ou APM, finalement ni le langage ni vraiment .NET ne levaient ne serait-ce que le petit doigt pour nous aider… Le Framework suivait bien le mouvement en proposant des classes écrites selon le paradigme de l’instant, ce qui était déjà énorme, mais ce n’était que cohérence avec les préconisations de Microsoft et non pas une vraie mutation.

Alors TAP est venu et cela a tout changé. Mais il aura fallu attendre .NET 4.5.

TAP est basé non pas sur une Interface comme APM, ni sur une gestion d’évènement comme EAP, mais sur la notion de Tâche, bien plus proche déjà sur le plan sémantique de l’asynchronisme recherché qui consiste à exécuter des tâches en parallèle du thread principal d’une application afin que celle-ci reste réactive.

TAP : une vraie co-évolution de tout l’ensemble plateforme / langage

Plutôt qu’une norme ou qu’une guideline que le développeur doit supporter seul (ou presque), TAP est une véritable évolution, plutôt une co-évolution de plusieurs blocs : d’une part le langage avec l’arrivée de wait/async, d’autre part du Framework avec l’espace de noms System.Threading.Tasks et avec les nouveaux types Task et Task<TResult>.

Bien entendu tout cela réclame un “mode d’emploi”, une sorte de pattern qu’il faut bien suivre comme les autres méthodes qui furent proposées avant TAP.

La différence majeure entre TAP et ses prédécesseurs c’est qu’il n’est pas qu’un Pattern, il coïncide aussi avec l’évolution nécessaire du langage et du Framework, ce qui lui donne toute sa force, sa puissance et qui le rend facile à coder.

Il ne faut pas exagérer non plus, coder des tâches parallèles réclamera toujours plus d’attention, de savoir-faire et de métier que de coder quelques instructions séquentielles. TAP ne transforme pas les mauvais développeurs en expert du multitâche. Au contraire, comme LINQ ou bien d’autres techniques, TAP impose toujours plus de qualification …

Mais pour ceux qui ont cette qualification développer en suivant TAP est bien plus agréable et moins créateur de bogues que d’utiliser les anciennes techniques.

TAP : une autre écriture

Avec TAP on n’écrit plus les choses de la même manière ni on ne les utilise identiquement, et c’est d’ailleurs là tout l’intérêt !

Ainsi notre classe exemple deviendra en suivant TAP :

public class MyClass
{
    public Task<int> ReadAsync(byte [] buffer, int offset, int count);
}

On retrouve la simplicité d’écrire du code suivant le modèle synchrone des temps anciens… Une seule méthode, la nôtre, uniquement des paramètres utiles à celle-ci, et un résultat conforme à ce qu’on souhaite retourner. Aucune pollution du code par des éléments liés à des problèmes d’implémentation de l’asynchrone.

Ok. Je l’admets le résultat n’est pas un simple Int mais un Task<Int>. C’est d’ailleurs la seule chose qui permet de voir qu’on utilise TAP plutôt que le modèle synchrone, en dehors de la convention de nommage qui fait que la méthode se termine par le mot Async.

Une méthode utilisant TAP retourne ainsi soit un Task simple (ce qui revient à retourner void) soit un Task<TResult>, TResult étant le type qu’on aurait retourné dans une écriture synchrone de la méthode (comme l’entier de notre exemple).

Il faut noter aussi une autre différence avec l’écriture synchrone : les paramètres “ref” ou “out’ sont à bannir alors qu’ils étaient autorisés dans le mode synchrone. Si la méthode doit retourner des valeurs particulières en plus du résultat, c’est ce dernier qui sera modifié pour devenir un tuple ou une structure de données particulières.

Conclusion

Synchrone ou asynchrone selon APM puis EAP ou TAP, le code moderne réclame pas mal d’effort de la part du développeur. !

L’utilisateur n’est pas seulement devenu exigeant, c’est aujourd’hui un gros bébé joufflu capricieux qui utilise le zapping en permanence, arme de destruction massive dans un marché hautement concurrentiel. Retenir son attention et donc son argent – directement par la vente de logiciels ou indirectement par de la publicité – implique de le chouchouter.

Il est vrai que beaucoup d’informaticiens croyaient que leur code se suffisait à lui-même. Que savoir maitriser les moindre astuces de la précédence des opérateurs pour rendre son code hyper classe et impossible à maintenir était le must. Si l’utilisateur avait des difficultés à faire ce qu’il voulait c’est qu’il était idiot, mal formé, de mauvaise foi, voire tout cela à la fois ! Ils se trompaient, et ceux qui pensent encore comme cela se trompent toujours gravement. J’ai eu l’occasion de parler souvent d’UX – j’ai même tenu une conférence sur ce sujet aux Techdays – Je ne peux que renvoyer le lecteur intéressé à l’ensemble de ces documents ainsi qu’au Tome 4 “Design & UX” de la sérié All.Dot.Blog.

Une application ne peut être considérée comme de bonne facture que lorsqu’elle plait à l’utilisateur, lorsqu’il a envie de l’utiliser, lorsqu’elle répond rapidement à ses besoins, ne créée pas d’angoisse inutile (pas de friction cognitive notamment), d’attente inexplicables.
Pas d’attente. Surtout pas.
Dans un monde où tout va vite où les modes changent comme le vent, dans un monde de gros bébés joufflus se goinfrant de junk-food devant du streaming de séries américaines, l’attente est l’ennemi de l’application efficace et vendeuse (lien vers la vidéo).

Latence zéro.

Et latence zéro cela implique même sur un PC sur-vitaminé de programmer en mode asynchrone. Donc de comprendre toutes les techniques qui permettent de maitriser cette contrainte loin d’être la plus facile à satisfaire sans un investissement intellectuel minimum.

Je n’entrerai pas dans les détails de TAP ici car j’en ai déjà parlé et que j’y reviendrai plus en détail dans un autre billet.

Allez, je vous laisse réfléchir à tout ça, vous pouvez-retourner faire cinquante choses à la fois et zapper frénétiquement comme tout bon consommateur que nous sommes tous ! Sourire

Stay Tuned!

blog comments powered by Disqus