Contexte
Dans l’étape précédente, j’avais promis de revenir sur Async, avec une application plus adaptée.
Pour ceux qui prennent en cours, ces quelques articles ont pour but de faire découvrir Async en montrant toutes les étapes de découverte, y compris et surtout les échecs. Ne prenez donc surtout pas le contenu pour argent comptant : il s’agit de manipulations et de bidouilles destinées à montrer la progression d’une personne pour appréhender les concepts d’Async, en espérant que ça serve à d’autres.
Nous allons donc repartir ce coup-ci d’une application avec GUI. Le problème en effet avec l’application de type Console est qu’elle est fondamentalement basée sur un thread de déroulement ordonné, unique, et donc synchrone. Nous avons besoin d’une application graphique pour montrer le réel intérêt d’Async.
L’application
Pour cela, une bête application simpliste de type WPF suffira. Un bouton pour lancer une commande, une TextBox pour afficher un résultat variable, et roule !
Si on travaille en mode synchrone, on utiliserait ce genre de code :
Evidemment, entre le moment où on clique sur le bouton et le moment où la GUI rend la main, impossible d’interagir avec elle, car tout fonctionne dans le même thread, qui s’occupe ici de l’affichage et de l’appel à un site web.
C’est précisément le scénario type pour lequel Async a été créé.
Solution à l’ancienne
Le problème étant posé, nous allons d’abord voir comment il était possible, avant Async, de le résoudre. L’idée était de mettre en place de l’asynchronisme, c’est-à-dire la séparation du traitement qui se déroule en parallèle de la gestion de la GUI. Pour cela, on utilisait un évènement de type Completed, et on associe un traitement qui s’occupe de récupérer le résultat du téléchargement pour le traiter :
Le problème est qu’on a tellement séparé qu’on se retrouve avec les deux parties du traitement qui se trouvent dans deux fonctions distinctes. Ceci pose problème, car on rompt la lecture du code, et ainsi qu’on ne manifeste pas correctement l’intention du développeur.
Solution de type Async
C’est pour cela qu’il fallait quelque chose de plus propre, comme Async. Async permet de concentrer l’asynchronisme dans une seule et même fonction, qui est syntaxiquement très proche du premier code que nous avions montré et qui était synchrone. Il “suffit” d’utiliser une fonction asynchrone qui renvoie une Task<string>, de rajouter le mot-clé await pour signifier que la TextBox sera remplie lorsque cette tâche sera terminée, et de mettre devant la fonction le mot-clé async, qui est nécessaire dans toute méthode employant l’asynchronisme :
Le gros avantage est bien sûr qu’on est très proche du code synchrone, et que la complexité est masquée. En arrière plan, c’est bien une vraie machine à état qui a été déclenchée. Si on jette un oeil avec ANTS Performance Profiler (attention, seule la toute dernière version Beta est capable de gérer de l’asynchronisme), on voit ce mécanisme sur la fonction marquée d’un tag Async (celle sélectionnée), ainsi que dans le code source retrouvé par décompilation (panel du bas), où on voit AyncStateMachine dans les attributs de la méthode :
Si on ouvre le graphique correspondant à cette méthode, le profileur de Red Gate nous montre bien le contenu déroulé par cette machine à état :
On commence par un Start, puis un MoveNext qui nous amène sur la fonction de téléchargement au final.
Pourquoi cette machine à état ? Tout simplement parce que le mode d’Async est de ne pas utiliser un thread à part, mais bien de rester dans le thread principal en utilisant une machine à état qui se déroule dans ce même thread. Ceci permet d’éviter les problèmes de basculement de threads, traditionnellement associés au retour d’un thread de traitement dans le thread principal, celui de la GUI, pour l’affichage associé en retour.
Gérer l’attente
Après avoir abordé cet exemple simple (et sans avoir parlé de ce qui se passe dans DownloadStringTaskAsync, qui mériterait un autre article le jour où j’aurai suffisamment bien compris le contenu pour en parler), je fais juste un petit détour par autre chose qui m’a permis de comprendre un peu mieux Async. C’est tout bête, mais comme ces articles sont censés montrer tout le cheminement étape par étape de la compréhension d’Async (en tout cas, mon cheminement, en espérant qu’il conviendra à expliquer les concepts à d’autres), je le fais voir quand même.
Supposons que le clic sur un bouton appelle une fonction Attente définie comme ceci :
Logiquement, la GUI bloquera pendant les 3 secondes. Si on fait quelque chose qui pourrait paraître similaire dans Async, on a un comportement différent :
En effet, pendant les trois secondes, on peut manipuler l’interface. Ce qui se passe est qu’on est entré dans la machine à états, que le thread est repassé au traitement de la GUI, et qu’au bout de trois secondes, le thread est passé sur l’étape suivante de la machine à états, qui a consisté à redonner la main au code au niveau du await. Il se trouve qu’il ne fait rien, mais au moins pendant le délai, on n’a rien bloqué.
Conclusion
Voici pour un troisième article sur Async. Promis, il y en aura un autre quand j’aurai mieux compris ce qui se passe dans la fonction DownloadStringTaskAsync : est-ce qu’elle est elle-même déclarée async ? Comment fonctionne-t-elle ? Comment peut-on utiliser Async pour créer des fonctions avec le même type de fonctionnement ?