Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Fathi Bellahcene

.Net m'a tuer!
Async/Await: comprendre comment ca marche

Tout le monde est unanime pour dire que la programmation multi-thread et asynchrone est en train de devenir un sujet incontournable.

Beaucoup de choses sont arrivées avec le framework 4 pour le code parallèle (TPL, PLinq,…)  et bientôt, on va avoir le droit à l’api async/await pour la partie Async.

Au temps vous le dire de suite, j’ai pas tout de suite compris les subtilités apportés par Async et j’étais donc en mode (idiot) “Async? m’en fous, ca sert a rien, avec TPL on peut déjà tout faire” …MAIS, je me suis dit que si Microsoft l’avait sortie et intégré dans le framework, c’est que ca mérite quelques heures à faire joujoux avec.

après avoir installé la CTP d’async, ma première bonne surprise est la qualité et la richesse de la documentation (a quand la même chose sur les API TFS Clignement d'œil ?):

image

Pour le coup, il y a vraiment tout pour bien commencer.

Maintenant, entrons dans le vif du sujet: le code!

on va prendre un exemple simple; une classe qui contient deux méthodes qui simulent des traitements court (1s) et le second long (2s):

   1: public class AsyncExemple
   2:     {
   3:         private string TraitementLong()
   4:         {
   5:             System.Threading.Thread.Sleep(2000);
   6:             return "traitement long fini";
   7:         }
   8:  
   9:         private string TraitementCourt()
  10:         {
  11:             System.Threading.Thread.Sleep(1000);
  12:             return "traitement Court fini";
  13:         }
  14:     }

 

Avec les Tasks, si je souhaite effectuer les deux traitements en parallèle, il me suffit de faire ca:

   1: public void ParallelTaskRun()
   2: {
   3:     Task.Factory.StartNew(() =>{Console.WriteLine(TraitementLong());});
   4:     Task.Factory.StartNew(() =>{Console.WriteLine(TraitementCourt());});
   5: }

et si j’exécute le code suivant:

   1: Console.WriteLine("Début du traitement");
   2: AsyncExemple e = new AsyncExemple();
   3: e.ParallelTaskRun();
   4: Console.WriteLine("Fin du traitement");
   5: Console.ReadLine();

On a le résultat (attendu) suivant:

image

On a lancé nos deux traitements dans des Threads séparés, du coup on a le message de fin de traitement avant d’avoir ceux des traitements long et court ET le traitement court se fini avant le long.

Maintenant la même chose avec async, prenons le code suivant (on simule en  plus un chargement):

   1: public async void Run()
   2: {
   3:  
   4:     System.Threading.Thread.Sleep(5000);
   5:     Console.WriteLine("...fin du chargement");
   6:     string text1 = await TaskEx.Run(() => { return TraitementLong(); });
   7:     Console.WriteLine(text1);
   8:     string text2 = await TaskEx.Run(() => { return TraitementCourt(); });
   9:     Console.WriteLine(text2);
  10: }
 
On obtient le résultat suivant:
 
image
Là, ca devient intéressant. On remarque que :
  • le chargement s’effectue de manière séquentiel malgré le fait qu’il soit dans la méthode marquée async.
  • les traitements marqués avec await s’exécutent bien de manière asynchrone par rapport à la méthode appelante (le Thread sur lequel s’exécute la méthode qui appelle la méthode async) MAIS de manière séquentiel.
Ce qui est asynchrone est donc ce qui est préfixé avec le mot clé await et est relatif au contexte de la méthode qui appelle la méthode async.
 
Prenons un autre exemple pour clarifier cette dernière phrase:
 
   1: public void Run()
   2: {
   3:     TraitementLongAsync();
   4:     TraitementCourtAsync();
   5: }
   6:  
   7: public async void TraitementLongAsync()
   8: {
   9:     string text = await TaskEx.Run(() => { return TraitementLong(); });
  10:     Console.WriteLine(text);
  11: }
  12:  
  13: private async void TraitementCourtAsync()
  14: {
  15:     string text = await TaskEx.Run(() => { return TraitementCourt(); });
  16:     Console.WriteLine(text);
  17: }
 

On a encapsulé les tratiements dans des méthodes asynchrones distinctes et on les lance depuis une méthode Run…si vous avez bien compris ce que j’ai dit plus haut, vous devriez savoir quel va être le résultat qui est …

image

…le même qu’avec les Tasks ! logique Sourire

les méthodes se lancent de manière asynchrone par rapport au contexte de la méthode appelante Run.

On a donc:

  • la méthode Run appelle la méthode asynchrone chargée de lancer le traitement long.
  • Celle-ci lance le traitement long en asynchrone et rend la main à la méthode Run
  • qui lance la méthode chargée de lancer le traitement long
  • …vous avez compris la suite

On voit bien ici qu’il y a quelques subtilité à appréhender. Ce qu’il est important de comprendre également c’est que sur une console ca n’a pas beaucoup de sens d’utiliser async, par contre, lorsque vous avez à faire des applications graphiques, cela devient extrêmement puissant. Si on prend l’exemple du Thread UI abordé dans mon post précédent. avec async il suffit juste de faire ca pour ne plus avoir à se poser de questions:

   1: private async void button1_Click(object sender, RoutedEventArgs e)
   2: {
   3:     textBox1.Text = await TaskEx.Run(() =>
   4:     {
   5:         System.Threading.Thread.Sleep(2000);
   6:         return "toto";
   7:     });
   8: }

le code est tout de même beaucoup plus élégant et on a plus besoin de se demander comment on retourne sur le Thread UI….ou comme le dirait Squizz; “la classe! totaaaale”.

RQ: on a la possibilité de faire ca directement sur le code du bouton car il a le bon gout de ne rien renvoyer (void).

Pour finir et voir si vous avez bien compris, que ce passe t’il si j’exécute le code  suivant:

   1: private async void button1_Click(object sender, RoutedEventArgs e)
   2: {
   3:     textBox1.Text = await TaskEx.Run(() =>
   4:     {
   5:         System.Threading.Thread.Sleep(2000);
   6:         return "toto";
   7:     });
   8:  
   9:     System.Threading.Thread.Sleep(5000);
  10:  
  11:     textBox1.Text = await TaskEx.Run(() =>
  12:     {
  13:         System.Threading.Thread.Sleep(2000);
  14:         return "tata";
  15:     });
  16: }

sans l’exécuter bien sûr Sourire

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :
Posted: jeudi 16 février 2012 22:08 par fathi

Commentaires

rt15 a dit :

On clique sur le bouton.

Le thread de l'UI exécute le début de button1_Click. Il démarre une tâche (Un thread n°2), et sort de la méthode. Il peut donc continuer de traiter les messages windows et l'UI est réactive.

Au bout de deux secondes, le thread n°2 termine l'exécution du Sleep.

Le thread de l'UI remarque que la tâche correspondant au await est terminé. Il reprend l'exécution de la méthode là où l'avait laissé. Il récupère le résultat du thread n°2 et met à jour textBox1 avec "toto".

Puis il exécute le Sleep 5000. Pendant 5 secondes, l'interface ne répond plus.

Au bout des 5 secondes, le thread de l'UI, continue la méthode. Il démarre une nouvelle tâche qui est exécuté par un thread n°3 (Quoique je suppose que c'est un pool de thread qui est utilisé dans les tasks, donc le thread n°2 pourrait être réutilisé ici).

Une fois la tâche démarrée, le thread de l'UI peut recommencer à traiter les messages. L'UI redevient réactive.

Au bout de deux secondes, le thread n°3 se termine.

Le thread de l'UI le remarque, et reprend l'exécution de la méthode. Il met à jour textBox1 avec le "tata".

Donc globalement, le mot clé await fait que le thread qui l'"exécute" sauvegarde son contexte et démarre une "tâche asynchrone" (Task, méthode renvoyant une Task

<T>...)

Quand le thread n'a rien à faire (Genre un peu comme pour Application.Idle), il regarde si des "tâches asynchrones" ont terminé. Si c'est le cas, il restaure le contexte associé au await, récupère le résultat de la tâche et continue son exécution.

# février 17, 2012 11:12

rt15 a dit :

Un truc me perturbait. Dans le cas du thread UI, on voit bien qu'il a du temps pour regarder si des tâches ont terminé et reprendre l'exécution de ce qu'il y a après le await.

Mais que se passe-t-il dans le cas d'une application console, ou si le await est "exécuté" par un thread non GUI ? Il n'y a pas de "idle" dans ces cas là.

La réponse est que si le thread n'est pas un thread de GUI, ce n'est pas lui qui continue l'exécution après le await. Un thread de threadpool est utilisé à la place.

Pour être plus précis, si le thread qui "exécute" le mot clé await propose un SynchronizationContext(Cas du thread de GUI), alors c'est lui qui continuera l'exécution de la méthode après la fin de la tâche. Mais si le thread n'a pas de SynchronizationContext, alors c'est un thread d'un pool qui est utilisé.

# février 17, 2012 11:38

rt15 a dit :

Pour être plus rigoureux sur la dernière phrase...

Les threads peuvent avoir un SynchronizationContext spécifique. S'il n'en n'ont pas, un SynchronizationContext par défaut, basé sur un threadpool, est utilisé.

Le thread de GUI a donc un SynchronizationContext spécifique.

Un SynchronizationContext dispose surtout de deux méthodes "Post" (Asynchrone) et "Send" (Synchrone). Ces méthodes permettent de demander au SynchronizationContext d'un thread d'exécuter une delegate.

C'est au SynchronizationContext de voire comment ces delegates seront exécutés. Dans le cas d'un SynchronizationContext associé à un thread de GUI, il va exécuter lui même les delegates.

Dans le cas d'un SynchronizationContext par défaut, il exécutera lui même les delegates fournie avec Send, et il délèguera les delegates fournie avec Post à un threadpool.

Async/Await peut être assez piègeux finalement... Dans certains cas, la suite est exécuté par un thread que l'on connait, dans d'autre un thread que l'on ne connait pas. Et ça a une importance car on ne peut pas faire la même chose avec un thread de l'UI et un thread quelconque.

Globalement, le mécanisme GUI async/await revient donc plus ou moins à faire un PostMessage(CALL_THIS_METHOD, &amp;MyCallback, lpParameters) à la fin des tâches asynchrones. Quand le thread de l'UI fait un GetMessage et tombe sur CALL_THIS_METHOD, il appelle MyCallback en lui passant lpParameters.

Là différence avec async/await, c'est que tout se passe comme si le thread UI n’exécutait pas une callback, mais reprenait l'exécution au milieu d'une méthode (Comme si le context du thread était véhiculé).

# février 17, 2012 18:10

rt15 a dit :

"Dans le cas d'un SynchronizationContext par défaut, il exécutera lui même les delegates fournie avec Send, et il délèguera les delegates fournie avec Post à un threadpool."

Non... Dans le cas du SynchronizationContext par défaut, c'est le thread qui appelle le Send qui exécute le delegate. Le thread n°2 fait un send sur le SynchronizationContext du thread n°1, mais le SynchronizationContext fait exécuté le code directement par le thread n°2. Ce qui fait toute la différence avec les SynchronizationContext des UI.

http://msdn.microsoft.com/en-us/magazine/gg598924.aspx

# février 18, 2012 10:08
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Créer un périphérique Windows To Go 10 ! par Blog de Jérémy Jeanson le 11-21-2014, 04:54

- RDV à Genève le 12 décembre pour l’évènement “SharePoint–Office 365 : des pratiques pour une meilleure productivité !” par Le blog de Patrick [MVP Office 365] le 11-19-2014, 10:40

- [IIS] Erreurs web personnalisées par Blog de Jérémy Jeanson le 11-19-2014, 00:00

- BDD/TDD + Javascript par Fathi Bellahcene le 11-16-2014, 16:57

- Sécuriser sans stocker de mots de passe par Blog de Jérémy Jeanson le 11-15-2014, 08:58

- Où télécharger la preview de Visual Studio 2015 ? par Blog de Jérémy Jeanson le 11-13-2014, 21:33

- Les cartes sont partout ! par Le blog de Patrick [MVP Office 365] le 11-13-2014, 17:26

- [ #Office365 ] Courrier basse priorité ! par Le blog de Patrick [MVP Office 365] le 11-12-2014, 08:56

- [Oracle] Fichier oranfsodm12.dll absent du package client par Blog de Jérémy Jeanson le 11-10-2014, 20:44

- [ #Office365 ] Le chapitre 1 des Groupes est écrit, et alors ? par Le blog de Patrick [MVP Office 365] le 11-10-2014, 20:23