Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Les nouveaux chemins vers l’asynchronisme – VI

Poster un commentaire

Le sixième volets d’une série:

  1. https://amethyste16.wordpress.com/2014/10/02/les-nouveaux-chemins-vers-lasynchronisme-i/
  2. https://amethyste16.wordpress.com/2014/10/03/les-nouveaux-chemins-vers-lasynchronisme-ii/
  3. https://amethyste16.wordpress.com/2015/03/10/les-nouveaux-chemins-vers-lasynchronisme-iii/
  4. https://amethyste16.wordpress.com/2015/03/15/les-nouveaux-chemins-vers-lasynchronisme-iv/
  5. https://amethyste16.wordpress.com/2015/03/17/les-nouveaux-chemins-vers-lasynchronisme-v/

La version .Net 4.5 nous a gratifiée d’un petit bijou: async/await qui va nous permettre d’écrire du code asynchrone avec une syntaxe de code synchrone. Donc un gain considérablement en simplification, mais aussi son lot de petites subtilités. Allons à la découverte de ce nouveau pattern.

Notez tout de même un détail. Async/await ne dispense absolument pas de connaître Task et son écosystème, bien au contraire. Donc si vous tombez sur cet article pour découvrir l’asynchronisme, je vous conseille vivement de lire les précédents.

Découverte d’async et await

Async est un nouveau mot clef qui s’applique à une méthode.

Par lui même il ne fait rien de particulier si ce n’est indiquer au compilateur qu’un ou plusieurs mots clefs await vont apparaître dans la méthode. Remarquez bien qu’en l’absence de await, la méthode est une méthode synchrone tout à fait normale, simplement on ne peut pas utiliser await dans une méthode qui n’a pas été marquée avec async.

Await est donc le moteur du pattern. Son rôle est de désigner le code qui devra être exécuté de façon asynchrone. A qui? Au compilateur. Comme nous le verrons plus loin, celui-ci va en effet réécrire le code en profondeur.

A t’on vraiment besoin de deux mots clefs? Pourquoi pas un seul des deux? Je ne connais pas les arguments qui ont menés à notre binôme, mais selon certains blogs la question a fait l’objet d’intenses discussions en interne.

 

Jusqu’à présent nous avons écrit du code dont le prototype pourrait être:

void DoSomething()
{
    WorkThatSentLaSueur()
    .ContinueWith(t=>
    {
        string retour=t.Result;
        Console.WriteLine(retour);
    });
}

Voici le même code écrit avec async/await:

async void DoSomething()
{
    string retour = await WorkThatSentLaSueur();
    Console.WriteLine(retour);
}

Prenez le temps de bien observer ce qui distingue ces deux codes même si le rôle de async/await n’est pas encore clair. Moins de code certes, mais comparez la syntaxe à celle d’un code purement synchrone:

void DoSomething()
{
    string retour = WorkThatSentLaSueur();
    Console.WriteLine(retour);
}

La structure est exactement la même, mais on devine même sur cet exemple très basique, le travail que va accomplir le compilateur dans les coulisses. C’est là que réside le génie d’async/await.

Contrairement à ce que le mot await suggère, il n’attend rien. La méthode se comporte vraiment comme si on avait un ContinueWith. et le code poursuit son exécution normalement.

 

Une dernière remarque avant de passer à la suite. J’expliquerai pourquoi plus tard, mais marquer avec async une méthode void est considéré comme une mauvaise pratique. Voyez ce code plutôt comme un prototype.

Mise en œuvre pratique du patron async/await

Puisqu’il s’agit d’une suite je vais bien entendu reprendre une partie du code écrit dans les articles qui précèdent. Nous allons les passer à la moulinette async/await.

Faisons une ovation à ce code qui a déjà été présenté dans un précédent article:

public static Task<string> LoadStringAsync()
{
    WebClient webClient = new WebClient();

    Task<string> downloadTask = webClient
        .DownloadStringTaskAsync("http://localhost/DemoWebapi/api/...");

    Console.WriteLine("Travail en cours...");

    return downloadTask.ContinueWith(t =>
    {
        webClient.Dispose();
        return t.Result;
    });
}

Il n’est pas difficile de voir l’analogie avec le template de code du chapitre précédent. La principale différence est que la méthode n’est pas void.

Le pattern async/await nous donne:

public static async Task<string> LoadStringAsync()
{
    using (WebClient webClient = new WebClient())
    {
        string retour = await webClient
            .DownloadStringTaskAsync("http://localhost/DemoWebapi...");
        Console.WriteLine("Travail en cours...");
        return retour;
    }
}

Et la preuve que le code fonctionne comme avant:

2015-03-23_23-22-39

Les marqueurs du pattern sont présents:

  • méthode décorée de async, c’est indispensable pour utiliser await
  • présence du mot clef await

DownloadStringTaskAsync retourne un Task<string>. Pourtant nous récupérons une String. C’est parce que await va « dépiler » pour nous Task et faire un appel à Task.Result.

Tout aussi intéressant est le retour de la méthode. On attend un Task<string>, pourtant on retourne une String. Là aussi c’est parce que le compilateur va nous aider un peu.

Au final, notre méthode sera bien asynchrone et c’est bien entendu ce que l’on observe.

Côté appel il n’y a rien de particulier. Async/await ne donne pas de propriétés magiques, c’est transparent pour le code client qui consomme la méthode.

Await apporte une souplesse inconnue avec ContinueWith, on aurait pu réécrire ce code ainsi par exemple:

public static async Task<string> LoadStringAsync()
{
    using (WebClient webClient = new WebClient())
    {
        Task<string> retour = webClient
            .DownloadStringTaskAsync("http://localhost/...");
        Console.WriteLine("Travail en cours...");
        return await retour;
    }
}

Notez la différence de l’emplacement de await. On n’a nullement besoin de le positionner sur la méthode appelée. Await gère le retour, pas l’appel.

Le dernier point remarquable apparaît si l’on compare avec son équivalent avec Task:

public static Task<string> LoadStringAsync()
{
    WebClient webClient = new WebClient();

    Task<string> downloadTask = webClient
        .DownloadStringTaskAsync("http://localhost/DemoWebapi/api/Fakeload?duration=10000");
    Console.WriteLine("Travail en cours...");

    return downloadTask.ContinueWith(t =>
    {
        webClient.Dispose();
        return t.Result;
    });
}

Avec async/await nous n’avons plus besoin de nous soucier des using, des foreach… Ce pattern nous permet d’écrire du code avec exactement les même habitudes qu’en synchrone. La preuve est sur cet exemple facile à apporter. Plaçons un point d’arrêt sur l’appel à Dispose grâce à une fonctionnalité découverte récemment sur ce blog:

Dans le menu Debug/New Break Point/Break at Function:

2015-03-24_21-39-20

  • On saisit (1) la fonction System.ComponentModel.Component.Dispose.
    Pourquoi pas WebClient? Parce que WebClient hérite de Component qui est le propriétaire de Dispose.
  • on fait OK
  • On fait OUI (2) sur la popin

La fenêtre des points d’arrêt ressemble à ceci:

2015-03-24_21-38-39

C’est notre point d’arrêt.

On lance l’application (F5). Au bout d’un moment, l’application s’arrête sur le point d’arrêt:

2015-03-24_21-36-34

Ce qui est intéressant est de regarder la console:

2015-03-24_21-36-53

Au moment où se déclenche le point d’arrêt, le code s’est déroulé normalement et en particulier est ressortit de LoadStringAsync sans déclencher Dispose. C’est seulement lorsque la Task a terminée sont travail que l’on sort du bloc using et déclenche Dispose exactement comme si le code était synchrone.

Note: pour que ça marche il faut désactiver l’option Enable just my code dans les options de débogage de VS

 

Si une telle souplesse est possible, c’est parce que dans les coulisses le compilateur n’utilise pas ContinueWith, mais réorganise le code en profondeur avec une machine à états. Microsoft a fait beaucoup de R&D à ce sujet et a estimé que c’est à la fois plus facile de générer un tel code et surtout beaucoup moins difficile de s’assurer que le code généré est correct.

Il est hors de mon propos d’expliquer comment les choses se passent, mais voici de la lecture:

Notez tout de même plusieurs points:

  • Il n’est pas possible d’utiliser await dans des getter et des setter (en même temps…)
  • pas de await dans un bloc lock
  • pas de await dans catch ou finally

Async sur une méthode void?

Dans la plupart des cas ce n’est pas considéré comme un bon pattern. mais il y a des exceptions notables, en particulier les gestionnaires d’événements.

Le problème des méthodes void est qu’elles n’ont pas de type de retour et en l’occurrence ne retournent pas de Task. Il devient alors impossible de contrôler l’apparition d’exceptions ou de lancer une annulation de la Task. Cela peut être dangereux (j’ai évoqué ce point dans l’article précédent), de plus vous n’avez plus moyen de savoir quand a terminé la Task.

Alors comment transformer un void en Task?

Regardez cette méthode et repérez en particulier la position de await qui est une autre démonstration de l’étonnante souplesse de ce pattern:

public async void TestVoidCase()
{
    using (WebClient webClient = new WebClient())
    {
        Task<string> retour = webClient
            .DownloadStringTaskAsync("http://localhost/DemoWebapi/api/Fakeload?duration=10000");
        Console.WriteLine(await retour);
    }
}

Comment transformer un void en Task? C’est très simple, on remplace void par Task, c’est tout!

public async Task TestVoidCase()
{
    using (WebClient webClient = new WebClient())
    {
        Task<string> retour = webClient
        .DownloadStringTaskAsync("http://localhost/DemoWebapi/api/Fakeload?duration=10000");
        Console.WriteLine(await retour);
    }
}

Il faudrait décompiler pour en être certain, mais il y a probablement du TaskCompletionSource derrière.

Ce n’est pas toujours possible. Par exemple les gestionnaires d’événement attendent une certaine signature et ce n’est pas Task, mais void. Il faudra donc gérer soi même les exceptions et s’assurer que l’application a bien terminée au bon moment.

Conclusions

Si l’asynchronisme vous intéresse, un des blogs de référence est celui de Stephen Toub. Voici d’ailleurs une FAQ dont je recommande la lecture:

http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/async-await-faq.aspx

Et ceci:

http://blogs.msdn.com/b/pfxteam/archive/2013/01/28/psychic-debugging-of-async-methods.aspx

Ainsi que des bonnes pratiques:

https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Bibliographie

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s