Contexte
Dans l’article précédent, j’ai déroulé mes premiers tests en Async, histoire de voir si l’apprentissage par l’erreur pouvait aider à comprendre comment fonctionne cette technologie. Nous nous étions quittés sur une question, à savoir s’il y avait plus simple / élégant / recommandé pour réaliser une exécution de code lors de la fin d’un appel asynchrone. La méthode asynchrone renvoyant au final une valeur, nous souhaitions pouvoir par exemple utiliser la valeur pour appeler une autre fonction, sans nécessairement compliquer le code avec des ContinueWith.
On continue de creuser
D’après ce que je lis ça et là sur le sujet, il doit être possible d’écrire cette ligne plus simplement :
On va tenter l’écriture avec un await :
Problème habituel : on ne peut pas mettre un await sans que la fonction soit déclarée en async (ça commence à rentrer…) et on essaie donc de mettre un async sur le Main, avec comme un vieux doute :
Effectivement, ça plante :
Ce n’est pas grave : on va juste extraire la méthode (en utilisant la refactorisation de Visual Studio bien sûr) pour la mettre dans une fonction async :
On donne le nom d’Appel à la fonction à créer, et on obtient un code qui compile sans erreur :
Par contre, il y a quand même un avertissement :
Effectivement, si on exécute tel quel, on a toujours le même souci, à savoir que comme on va arrêter la thread Main avant que le message en retour ne soit traité, on ne verra pas la fin du traitement :
Pour valider que c’est bien le cas, on ressort notre astuce précédente avec un Thread.Sleep(3000) dans le Main :
Le résultat montre que dès le premier affichage de res, on a maintenant une valeur :
Du coup, pas besoin de boucler pour attendre qu’il soit différent de la valeur par défaut, et on peut écrire simplement :
Mais du coup, on retombe finalement avec ce genre d’appel sur un enchaînement synchrone…
Point à mi-parcours (OK, à 10% du parcours)
En fait, arrivé là, je me demande si le problème ne vient pas de ce que j’essaie de comprendre l’asynchronisme à partir d’une application en mode console, qui fonctionne de façon séquentielle, et donc fondamentalement synchrone. Du coup, je vais plutôt continuer avec une application test Winforms, histoire de voir les effets sur une application qui utilise une pompe de message. L’hypothèse est que, comme l’await dans notre cas revient au comportement synchrone, il doit quand même y avoir une différence, mais elle n’est pas visible…
Quelques semaines plus tard
Arrivé à cette étape, je me suis rendu compte que j’avais besoin de lire de la doc avant de continuer à faire mes tests en aveugle depuis le code, sous peine de ne pas arriver à grand chose. Entretemps, une lecture du blog de Julien Dollon (http://julien.dollon.net/post/Async-is-not-ALWAYS-your-friend.aspx) et un commentaire de sa part m’ont aidé à avancer en partageant l’asynchronisme et la concurrence par les tâches.
Cette différence, fondamentale mais pas si facile à capter, est bien posée dans l’excellent blog de Jérôme Laban, dans la partie 1 de ses posts sur Async. Je vous mets les liens, parce que ce sont trois très bons articles :
http://www.jaylee.org/post/2012/06/18/CSharp-5-0-Async-Tips-and-Tricks-Part-1.aspx
http://www.jaylee.org/post/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void.aspx
Le premier lien m’a permis de comprendre qu’effectivement, je ne pourrais pas aller beaucoup plus loin dans la compréhension d’Async avec une application console. L’auteur explique qu’Async ne s’occupe pas de concurrence, mais simplement d’asynchronisme par la mise en place (par le compilateur) d’une machine à état, qui fait progresser les différentes étapes par les mots clés await. Hors, une des difficultés est justement que la synchronisation du contexte ne donne pas la garantie de retomber sur le thread UI, ou sur le thread nécessaire pour que des opérations dépendantes du thread fonctionnent.
Bref, ça conforte l’idée qu’il faut repartir d’un autre exemple avec une interface, ou en tout cas deux threads. C’est d’une certaine manière ce qui a forcé la mise en place de Task dans les exemples précédents, alors que les notions sont séparées.
Cet article a également participé à me faire comprendre qu’Async ne mettait pas fondamentalement en place de la concurrence, car il fonctionne à la base sur un thread.
Quelques notes de plus
En fait, await n’attend pas tout de suite, mais il est le signal que la tâche représentée par la fonction sera considérée comme terminée lorsqu’elle aura fini la commande marquée comme await.
public async Task Initialize()
{
//Thread.Sleep(500);
await Task.Delay(500);
}
Thread.Sleep fait attendre tout le monde. Task.Delay sans await lance un delai, mais on passe 3 ms au final, car la fin n’est jamais attendue. Quand on met un await, on n’attend pas la fin de chaque tâche, mais on donne la possibilité au cas appelant de connaître la fin de ces tâches, bien qu’elles aient été lancées à la suite sans délai. Du coup, on est à 503 ms…
Au programme de la troisième étape
Dans le prochain article sur le sujet, je repartirai d’une application exemple qui va nous permettre de mieux voir le fonctionnement que mon application console, pas adaptée. Pour ceux qui souhaitent comprendre Async, désolé pour ce détour, mais je vous avais prévenu : ces articles ne sont pas des cours par quelqu’un qui connaît, mais un carnet de bord des étapes d’apprentissage de quelqu’un qui cherche à apprendre. C’est du coup plus tortueux, mais j’espère que ça adoucit la courbe d’apprentissage pour un débutant…
En attendant, je vous recommande à nouveau la lecture des trois articles sur le blog de Jérôme Laban. Perso, j’y retourne…