Comprendre Async

Il y a de très bons articles à propos d’Async sur le blog de Stéphanie Hertrich de Microsoft (ici pour expliquer les problèmes sur les actions longues, et pour une explication plus globale de l’asynchronisme et du parallélisme), ainsi que sur de nombreux autres blogs / sites, mais ce n’est pas facile de trouver une explication pour débutant absolu. Si comme moi, vous avez un peu de mal avec la marche conceptuelle pour comprendre Async, j’espère que les notes ci-dessous vous aideront à avancer.

AVERTISSEMENT : les bouts de code ci-dessous sont des tâtonnements, pour comprendre par l’erreur les principes d’Async. Ne vous étonnez donc pas si vous voyez des horreurs, c’est fait pour…

Warning CS1998

Le code ci-dessous lance un avertissement :

image

image

D’après ce que je comprends, le compilateur émet un warning en nous expliquant en gros que ce n’est pas la peine de mettre un mot clé async vu qu’on ne fait pas d’await dans la fonction, et que du coup, le fonctionnement final est équivalent à un fonctionnement synchrone. On constate d’ailleurs ce comportement si on exécute tout de même :

image

Le code est donc complètement équivalent à celui-ci :

image

Retour des fonctions async

Une autre chose qui permet de comprendre un peu mieux le principe est qu’une fonction async ne peut pas retourner directement une valeur. Dans le code ci-dessous, ce n’est plus un avertissement, mais bien une erreur de compilation qu’on obtient :

image

On a encore l’avertissement précédent, ainsi qu’un second sur l’appel de la fonction marquée comme “async” dans la méthode Main :

image

Le plus riche d’enseignement est l’erreur : une fonction marquée async doit renvoyer void ou bien une tâche.

Note : je fais exprès de dire “marquée async” et non pas “une fonction async”, car d’après ce que je comprends pour l’instant, le mot clé ne transforme pas la méthode en méthode asynchrone : il indique que des traitements dans cette fonction vont l’être. Ce qui rend une méthode asynchrone, c’est la présence des await dans son corps)

Pour en revenir au retour, essayons de comprendre à quoi peut correspondre cette Task (simple ou générique) en retour. On imagine que le type générique est peut-être le type du retour éventuellement attendu par les appelants de cette fonction.

Retour d’une Task<T>

Essayons donc l’exemple suivant :

image

On n’a plus d’erreur de compilation, car la signature est Task<string>. Remarquez tout de même au passage que le return renvoie res, qui est déclarée dans le corps comme une variable de type string, et pas Task<string>. Voilà donc un truc qu’Async fait : il y a eu modification du compilateur pour qu’en présence du mot clé aysnc dans la déclaration de la fonction, le return d’un T compile avec une signature renvoyant un Task<T>. Intéressant…

Evidemment, si on n’avait pas async dans la déclaration, on aurait une erreur de compilation sur le return :

image

Au passage, il y aurait d’ailleurs une seconde erreur, qui est le pendant du warning CS1998, parce qu’on utilise un await dans une fonction qui n’est pas marquée “async” :

image

Mais revenons à nos moutons… Si on remet l’async, le code plus haut compile, mais tout de même avec un avertissement, ce coup-ci sur l’appel de la fonction LancementAsync depuis la méthode Main :

image

Si on lance l’application telle quelle, le comportement change. On a le comportement asynchrone du Main, en ce sens où il n’attend pas la fin de la fonction :

image

Utilisation du retour

Effectivement, dans notre cas, on a créé la fonction avec un mot clé await, qui déclenche le comportement asynchrone du traitement d’exécution. Plaçons des traces dans le code de la fonction Executer() comme ceci :

image

On constate effectivement que le lancement est bien réalisé, mais la fin jamais atteinte. Async doit lancer une tâche quelque part dans un thread qui n’empêche pas l’application de se terminer lorsque le thread principal de traitement qui exécute Main arrive à la fin de son travail :

image

Du coup, si on veut vraiment utiliser le résultat de la fonction LancementAsync (pour l’afficher, par exemple), on peut imaginer ce genre de code, qui utilise la tâche retournée comme tâche de continuité de traitement :

image

Ce coup-ci, tout compile, mais…

On ne voit toujours rien !

En effet, quand on lance, on ne voit pas la fin de la fonction Executer, ni le retour du résultat pour autant :

image

Mais pour le coup, il y a de bonnes chances que ce soit juste parce que, Main étant arrivé à la fin (justement parce que l’appel est désormais non bloquant), le processus est interrompu. Faisons quelque chose de bien pourri pour retarder sa fermeture, juste pour confirmer :

image

Au risque de la répétition, je préfère rappeler que tous les bouts de code ici présents ne sont que des essais pour essayer de comprendre PAR L’ERREUR comment fonctionne Async. Donc, ne prenez évidemment pas ce code pour quelque chose de propre ou même simplement utilisable : ce n’est pas le cas !

Là, on commence à avoir quelque chose :

image

L’affichage est un peu bizarre : c’est typiquement le ToString sur une classe qui ne le surcharge pas, et pas l’affichage de notre chaîne en retour… Voyons voir ce qui peut se passer :

image

Visual Studio nous aide : le ContinueWith attend un délégué de type Action, non pas sur un string, mais sur un Task<string>. Il doit bien y avoir quelque chose là-dedans qui nous permet de retrouver notre résultat, comme une  propriété Value ou quelque chose approchant…

Et bien, c’est Result, tout simplement :

image

Ce qui nous donne le résultat attendu :

image

L’intérêt

Arrivé là, on a déjà une première idée de l’intérêt de tout cela, en ajoutant juste une trace qui permet de mieux voir ce qui se passe :

image

Ce qui simule bien ce qui nous intéresse, à savoir continuer le parcours d’un programme (d’une GUI, d’une exécution centrale, d’une boucle d’attente de commande, etc.) tout en ayant une autre partie du programme qui s’exécute, avec un traitement postérieur si nécessaire :

image

Et si on veut attendre le résultat ?

Changeons encore une fois le code, pour ce coup-ci remplir une variable avec le résultat au lieu de l’afficher dans la console :

image

Conformément à ce qu’on attend, on se retrouve avec un premier affichage avec la valeur initiale et un second, lorsque le temps suffisant est écoulé, avec la valeur affectée par l’exécution :

image

Question : dans du vrai code, on ne sait pas à l’avance combien une méthode va prendre de temps, et on peut donc se retrouver avec un endroit (typiquement, notre second affichage de res) où on souhaite utiliser tout de suite la chaîne si elle a été valuée, et sinon bloquer en attendant qu’elle le soit. Comment peut-on réaliser ceci ?

Evidemment, on peut travailler de manière sale (vu comment on se vautre dans le code pourri sur ce post, allons-y gaiement) :

image

Ca n’est évidemment pas la bonne façon de faire, car on boucle en bloquant le thread (et en plus sans donner à un autre thread la main), mais ça a au moins le mérite de supprimer le besoin du Thread.Sleep sur 3000 millisecondes, et on peut constater par le rajouter de l’affichage final de l’heure qu’on est sur les 2000 millisecondes du traitement Executer() :

image

Fin en queue de poisson

Vous aimeriez connaître la solution ? Et bien moi aussi, mais je ne l’ai pas ! Donc rendez-vous au prochain épisode pour la suite. Sachant que, si ça se trouve, la bonne façon de faire est de rester dans le mode ContinueWith qu’on a trouvé au début… Evidemment, les commentaires sont plus que jamais bienvenus pour me faire comprendre Async Sourire. En espérant que cet article avec une approche différente (apprendre par l’erreur) vous aura plus aidé qu’embrouillé. Dans le dernier cas… caveat emptor !

About JP Gouigoux

Jean-Philippe Gouigoux est Architecte Logiciel, MVP Connected Systems Developer. Il intervient régulièrement à l'Université de Bretagne Sud ainsi qu'à l'Agile Tour. Plus de détails sur la page "Curriculum Vitae" de ce blog.
This entry was posted in .NET and tagged . Bookmark the permalink.

8 Responses to Comprendre Async

  1. oaz says:

    C’est intéressant. J’attends la suite !
    Je n’ai pas encore eu l’occasion de m’intéresser aux évolutions récentes de C# mais en voyant ce “async” (que je découvre donc) je me demande : y a-t-il une différence fondamentale avec le BeginInvoke des delegates qui lui est là depuis un bon bout de temps ?

    • JP Gouigoux says:

      Bonjour,

      Etant un lecteur de ton blog (je me suis particulièrement retrouvé dans http://agilitateur.azeau.com/post/2011/04/13/Avant-de-se-mettre-%C3%A0-%C3%A9crire%2C-il-faut-apprendre-%C3%A0-lire), je suis honoré de recevoir tes commentaires !

      D’après la (faible) connaissance que j’ai d’Async, j’ai l’impression qu’il n’y a pas de différence fondamentale avec ce qui se passait sur les BeginInvoke. En arrière plan, il y a toujours un thread / une tâche qui se lance de manière parallèle, et qui offre la possibilité grâce à un IAsyncState / une ContinuationTask, de déclencher un comportement lors de la fin de cette opération, quel que soit l’état d’avancement du thread principal.

      Maintenant, comme je le disais dans ce post, je débute à peine, donc j’y verrai j’espère plus clair dans quelques semaines. Je garde cette question au chaud pour le ou les articles qui suivront.

      JP

  2. JulienD says:

    Salut JP
    Super Article.
    Moi ce qui me fait peur avec Async c’est que les gens l’utilisent a tord et a travers.
    Je m’explique, imaginons nous ayons un appel a une DataBase pour lire des clients et que chaque client doit etre initialize:

    foreach(var client in clients)
    client.Initialize();

    Imaginons, que nous voulons que la methode Initialize soit executee dans un autre thread. Car certes il faut initialiser tous les clients, mais nous ne souhaitons pas attendre que le premier client soit initialiser pour commencer a initialiser le second, nous voulons juste attendre que le dernier soit fini (en admettant que le temps d’init soit le meme pour tous, et que de ce fait, le dernier permette de dire que la liste des clients soit initialiser).

    J’aurai donc propose -> Execution du Initialize dans un background worker, puis si nous sommes dans le dernier, on s’abonne au callback pour continuer le deroulement de notre programme.
    Un truc du genre

    for(int i = 0; i {//CODE HERE });
    }

    et que du coup tu fais ca:
    foreach(var client in clients)
    await client.Initialize();

    La t’es content, ca t’as pris deux minutes et ton programme reste en deroulement “synchrone fake”.
    Sauf que tu auras du attendre que chaque client s’initialize avant de commencer le prochain.
    Si tu fais un Stopwatch tu vas pleurer niveau perf….

    Voila j’aimerai ton point de vue, ou voir meme un poste la dessus.
    La bise!

    • JulienD says:

      Mince mon commentaire est partie en vrac…
      Bon bah j’expose mon pb sur mon blog dans un blog post dsl JP!

      • JP Gouigoux says:

        Salut, Julien.
        Non, je pense que ton commentaire est bien passé. En tout cas, on le voit en clair. J’irai voir de temps en temps si tu postes du nouveau sur http://julien.dollon.net/, mais en attendant, je vais réfléchir à ce que tu exposes, et ça va me servir de base, comme le commentaire précédent de OAZ, pour creuser un peu plus.
        Stay tuned !

  3. Maxence says:

    Merci pour cet exemple, il m’a été très utile pour découvrir async/await.

    Pour attendre le résultat on peut utiliser la méthode Wait() sur une Task.
    Cela permet de s’assurer qu’une tâche est terminé avant de quitter l’application.

    Exemple où l’utilisateur doit saisir une ligne pour quitter, on attend que la tâche soit terminée afin de fermer le programme correctement :

    class Program
    {
    static Task task;

    static void Main(string[] args)
    {
    string res = "Sans valeur";
    task = LancementAsync();
    Console.ReadLine();
    AttendreFinDeTache();
    res = task.Result;
    Console.WriteLine(res);
    Console.WriteLine("Arret du programme");
    }

    private async static Task LancementAsync()
    {
    return await Task.Factory.StartNew(Executer);
    }

    private static string Executer()
    {
    Console.WriteLine("Début Executer");
    Thread.Sleep(4000);
    Console.WriteLine("Fin Executer");
    return "Resultat";
    }

    private static void AttendreFinDeTache()
    {
    if (task.IsCompleted)
    Console.WriteLine("Tâche terminée inutile d'attendre plus longtemps");
    else
    {
    Console.WriteLine("Patientez la tâche se termine");
    task.Wait();
    }
    }

    • David says:

      Merci pour ton code Maxence, mais ici tu fais tourner UNE unique tâche, qu’en est-il si je dois lancer 1000 tâches, comment savoir si TOUTES les tâches sont terminées avant de fermer l’application ? Je dois mettre cela dans une Queue ?

      merci

      David

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha Captcha Reload