Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Les nouveaux chemins vers l’asynchronisme – VII

Poster un commentaire

On continue la 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/
  6. https://amethyste16.wordpress.com/2015/03/24/les-nouveaux-chemins-vers-lasynchronisme-vi/

Avec quelque chose d’important: l’annulation d’une Task. Depuis .NET 4.0, Microsoft fournit pour cela un framework centré autour de CancellationTokenSource. Nous allons l’aborder dans le contexte de l’asynchronisme, mais ce modèle est suffisamment général pour être utilisé dans tout autre situation. Au programme:

  • Techniques de bases avec Task
  • Task et son constructeur
  • async/await

 

Le modèle est :

  • Générique
    Le modèle d’annulation est arrivé avec TPL, mais peut être utilisé dans tout autre situation sans rapport avec l’asynchronisme. Le mécanisme d’annulation n’est pas lié à Task, n’est pas déclenché par une méthode de Task ou de Thread. Il emporte sa propre plomberie.
  • Chaînable
    La commande d’annulation est transmise à la tâche maître, mais celle-ci à la possibilité de la transmettre à des tâches filles. Le mécanisme de déclenchement de l’annulation sera transmit lui aussi à ces tâches
  • Coopératif
    Il repose sur le fait que la tâche qui reçoit la d’annulation vérifie périodiquement son statut.
    Cela signifie que l’annulation ne sera pas forcément immédiate (elle est asynchrone), peut être même sera t’elle ignorée par le code qui la reçoit .
    Il est techniquement guère possible de faire autrement. La tâche en cours peut par exemple ne plus dépendre du code qui l’a initié (une requête de commande vers un autre processus, application ou même serveur par exemple). Par ailleurs le fait de ne pas être impératif, fournit un moyen de gérer proprement l’annulation (revenir à un état standard, fermer les ressources…).

Mise en œuvre pratique

Techniques de base

La classe CancellationTokenSource est la clef de voûte du modèle.

Nous allons commencer avec notre exemple habituel avant d’examiner le cas async/await un peu plus tard:

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;
    });
}

Le mécanisme étant indépendant de TPL cela signifie que l’on devra l’injecter dans notre code et donc transformer un peu sa signature. On fait cela ainsi:

public static Task<string> LoadStringAsync(CancellationToken ct)
{
    WebClient webClient = new WebClient();
    Task<string> downloadTask = webClient
      .DownloadStringTaskAsync("http://localhost/DemoWebapi/api/...");

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

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

L’exemple n’est pas super utile, mais il suffira bien à montrer les bases. Remarquez la signature de la méthode et l’appel à ThrowIfCancellationRequested qui teste si une condition d’annulation existe et lève l’exception.

Comment obtenir un CancellationToken et gérer une demande d’annulation? En l’instanciant dans le code client:

CancellationTokenSource cts = new CancellationTokenSource();
Task<string> taskRetour = LoadStringAsync(cts.Token);

On construit une instance de CancellationTokenSource et on fournit la propriété Token.

Note: Le constructeur expose deux autres surcharges qui permettent de lever automatiquement une demande d’annulation au bout d’une durée.

Nous allons le faire manuellement en modifiant notre IHM:

while (true)
{
    string key = Console.ReadLine();
    Console.WriteLine("Bîîîîîîîîîip");
    if (key == "c")
    {
        cts.Cancel();
    }
}

Si l’utilisateur presse la touche c, alors la méthode Cancel() est exécutée et une demande d’annulation est formulée. Il ne reste plus qu’à tester. Vous constatez que l’exception OperationCanceledException est levée.

Note: Il existe également la méthode CancelAfter() qui permet elle aussi de déclencher une demande d’annulation planifiée. Cette méthode est disponible dans .NET 4.5.

 

Cette méthode peut sembler brutale et on peut souhaiter faire un peu de ménage avant d’entamer une réaction. On peut réécrire le code ainsi:

return downloadTask.ContinueWith(t =>
{
    if (ct.IsCancellationRequested)
    {
        // on fait le nettoyage...
        ct.ThrowIfCancellationRequested();
    }

    webClient.Dispose();
    return t.Result;
});

La propriété IsCancellationRequested nous alertera si une demande d’annulation a été effectuée ce qui nous laisse tout le temps nécessaire pour lancer quelques opérations de terminaison.

Pourquoi alors ne pas faire un simple return à la place de l’appel à ThrowIfCancellationRequested?

Il est essentiel qu’en cas d’annulation le statut de Task passe à Canceled… ne serait-ce pour permettre à un code client de savoir qu’une annulation a eu lieu! Le déclenchement de méthode de continuation peut aussi en dépendre.

 

La méthode DownloadStringTaskAsync ne possède pas de surcharge admettant un CancellationToken, cela indique qu’il n’y a pas de support pour l’annulation. On est donc obligé d’attendre qu’elle se termine (si elle se termine!) pour tester l’annulation.

En pratique il serait d’ailleurs mieux de réaliser également le test avant de lancer DownloadStringTaskAsync. Cela a du sens parce que ce n’est pas parce qu’une Task est créée qu’elle est lancée immédiatement. Et d’ici là des tas de choses peuvent se passer…

public static Task<string> LoadStringAsync(CancellationToken ct)
{
    ct.ThrowIfCancellationRequested();
    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 =>
    {
        if (ct.IsCancellationRequested)
        {
            // on fait le nettoyage...
            ct.ThrowIfCancellationRequested();
        }
        webClient.Dispose();
        return t.Result;
    });
}

Le tests d’annulation peut donc apparaître autant de fois qu’on le souhaite dans le code.

Par contre, inversement, un CancellationTokenSource ne peut être utilisé qu’une seule fois. Vous ne pourrez pas réinjecter le CTS dans une autre méthode et refaire un Cancel pour l’annuler.

Task, son constructeur, l’annulation, son oeuvre

Examinez cet exemple inspiré d’un blog de John Badams:

CancellationTokenSource cts = new CancellationTokenSource();
Task.Run(
    () =>
        {
            while (true)
            {
               cts.Token.ThrowIfCancellationRequested();
            }
        }, cts.Token);

Le fonctionnement de ce code est clair à la lumière de ce qui vient d’être expliqué.

Ce qui l’est moins est pourquoi on a besoin d’injecter deux fois le CTS: une fois dans le constructeur et une autre fois dans la méthode déléguée.

La réponse est ici, on peut la résumer par:

  1. Si une annulation est reçue AVANT que la Task soit lancée, celle-ci ne démarrera pas et passera directement à Canceled.
    On évite ainsi le coût de lancer la Task
  2. ThrowIfCancellationRequested lève OperationCanceledException en passant dans les paramètre le jeton reçu.
    Si Task voit que ce jeton est le même que celui reçut dans son constructeur, alors Task bascule à Canceled plutôt que Faulted comme elle devrait le faire pour une exception ordinaire.

Faisons donc quelques tests:

Task task = Task.Run(
    (Action)(() =>
        {
            while (true)
            {
                cts.Token.ThrowIfCancellationRequested();
            }
        }), cts.Token);

La raison du cast par Action a été expliquée ici. On modifie l’IHM de cette façon pour afficher le statut de la Task et on lance:

while (true)
{
    string key = Console.ReadLine();
    Console.WriteLine("Bîîîîîîîîîip");
    if (key == "c")
    {
        cts.Cancel();
    }
}

2015-03-28_15-55-41

La première fois que l’on appuie sur c on lance une demande d’annulation et affiche aussitôt après le statut de la Task qui est encore Running puisque l’on est encore dans la méthode déléguée. La fois suivante on a quitté la boucle while la Task a changée de statut et Canceled s’affiche.

Essayez maintenant d’enlever le jeton du constructeur:

2015-03-28_15-56-57

Et cette fois le statut est Faulted ce qui n’est pas le résultat attendu.

Encore un résultat à ne pas oublier si on veut éviter certains bugs dans nos applications.

Ce que vous pouvez retenir est que Task.Run ne lèvent jamais TaskCanceledException. Celle-ci est levé lorsque la Task est attendu (await, Wait, Result…). On capture une AggregatedException ou bien une TaskCanceledException avec await.

Task implémente IDisposable. Doit t’on alors la disposer?

Stephen Toub fournit une réponse ici:

http://blogs.msdn.com/b/pfxteam/archive/2012/03/25/10287435.aspx

La version courte est: NON! Ou sinon il faut avoir une bonne raison comme un problème de performance, le problème sera alors de trouver un bon emplacement pour le faire.

Et async/await?

Techniques de base

Reprenons notre célèbre méthode LoadStringAsync et reprenons là à la sauce async/await:

public static async Task<string> LoadStringAsync(CancellationToken ct)
{
    ct.ThrowIfCancellationRequested();

    using (WebClient webClient = new WebClient())
    {
        string message = await webClient
           .DownloadStringTaskAsync("http://localhost/DemoWebapi/api/Fakeload?duration=10000");

        if (ct.IsCancellationRequested)
        {
            Console.WriteLine("Nettoyage sur annulation...");
            ct.ThrowIfCancellationRequested();
        }
        return message;
    }
}

La méthode attend un CancellationToken en paramètre. DownloadStringTaAsync n’a pas de surcharge avec CancellationToken, autrement on l’aurait utilisée. Je ne crois pas que le code présente de grosses difficultés.

Regardez maintenant le code suivant:

CancellationTokenSource cts = new CancellationTokenSource();
Task<string> task = LoadStringAsync(cts.Token);
task.ContinueWith( async t =>
{
    Console.WriteLine(t.Status);
    string message = await t;
    Console.WriteLine(message);
});

Ce qui est intéressant est de remarquer que task a toujours pour statut WaitingForActation, par contre t.Status passe à Canceled. Cela rappelle le comportement de Task.Run. Pour retrouver notre comportement avec les bons statuts, il faut faire confiance au pattern async/await qui fait très bien le boulot. Il devient alors possible d’intercepter OperationCanceledException:

static async void GetMessage(CancellationToken token)
{
    try
    {
        ct.ThrowIfCancellationRequested();

        string message = await LoadStringAsync(token);
        Console.WriteLine(message);
    }
    catch (OperationCanceledException)
    {
        // netoyage
    }
}

CancellationTokenRegistration

Il est parfois utile d’exécuter une action au moment où une demande d’annulation est formulée. Reprenons le code qui précède, mais avec une nuance:

[Code language= »csharp »]
CancellationTokenSource cts = new CancellationTokenSource();
cts.Token.Register(()=>
{
Console.WriteLine(« Nettoyage de printemps… »);
}
);

GetMessage(cts.Token);
[/code]

L’exécution montre ceci:

2015-03-29_19-12-50

L’action enregistrée est exécutée immédiatement, l’attente est simplement due à l’attente de DownloadStringTaskAsync.

Et TaskContinuationOption?

Que deviennent avec async/await les différentes options proposées avec ContinueWith et TaskContinuationOption? On en a pas besoin, il suffit de placer un try/catch. Regardons ce code:

Task.Run(() => { Meth1(); })
    .ContinueWith((t) => { Meth2(); }, TaskContinuationOptions.OnlyOnCanceled);

Se transforme en:

try
{
    await Meth1();
}
catch
{
    Meth2();
    throw;
}

Cette structure devrait suffire pour traiter tous les cas tels None, NotOnCanceled, NotOnFaulted, NotOnRanToCompletion, OnlyOnCanceled, OnlyOnFaulted, et OnlyOnRanToCompletion.

Les autres valeurs de l’énumération comme AttachedToParent, HideScheduler, et PreferFairness ne sont pas utilisés dans un contexte async/await.

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