Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Fathi Bellahcene

.Net m'a tuer!
Cancellation & Tasks

Les traitements parallèles/asynchrones présentent pas mal d’avantages et l’introduction des Tasks et d’async/await nous permettent de les utiliser (d’en abuser) simplement on améliorer la lisibilité de notre code.

Cependant, il reste un point qui, à mon avis, mérite un petit post parce qu’il n’est pas aussi trivial qu’il n’y parait: l’annulation des tâches en cours d’exécution.

Un petit exemple pour poser le problème:

J’ai une application qui doit charger des données et ce traitement prend un certains temps, d’un autre coté, j’ai la possibilité d’annuler ce chargement (pour une raison quelconque).

j’ai donc une application WPF qui ressemble a ca:

image

et le code suivant:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
 
        CancellationTokenSource cts = new CancellationTokenSource();
 
        void Button_Click_1(object sender, RoutedEventArgs e)
        {
            CancelTest1();
            //CancelTest2();
            //CancelTest3();
            //CancelTest4();
            //CancelTest5();
            //CancelTest6();
        }
 
 
        void CancelTest1()
        {
            listBox.Items.Add("start loading...");
            Task<string>.Run(() =>
            {
                System.Threading.Thread.Sleep(3000);
            },cts.Token).ContinueWith(t => listBox.Items.Add("finish"), 
TaskScheduler.FromCurrentSynchronizationContext());
        
        }
 
        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            cts.Cancel();
            listBox.Items.Add("Cancel!");
        }
 
    }

On utilise un CancellationToken (produit par l’objet CancellationTokenSource) on le passe en paramètre de la méthode Task.Run() qui est chargée de lancer mon traitement. Pour l’exécution de l’annulation, j’appel

simplement la méthode Cancel sur mon objet de type CancellationTokenSource (celui à partir duquel j’ai créer mon objet CancellationToken).

Ca marche pas!

Si je lance un chargement et que je l’annule dans la foulée, j’obtient le résultat suivant:

image

Je me rend compte que ma tâche ne s’annule pas! pire, elle termine sont traitement et maj mon interface!…bref, ca marche pas (…comme ca).

Ca marche TOUJOURS pas!

Du coup, je me dit que je doit tester le statut de la Tâche pour ne pas avoir à maj mon UI (temps pis si la tâche s’éxécute jusqu’au bout…dans  un premier temps). Je test donc le code suivant:

   void CancelTest2()
        {
            listBox.Items.Add("start loading...");
            Task.Run(() =>
                  {
                      System.Threading.Thread.Sleep(3000);
                  }, cts.Token).ContinueWith(t =>
                  {
                      if (t.IsCanceled)
                          listBox.Items.Add("finish...but canceled");
                      else if (t.IsCompleted)
                          listBox.Items.Add("finish");
                      else if (t.IsFaulted)
                          listBox.Items.Add("Error during loading");
 
                      listBox.Items.Add("Task status->" + t.Status);
                  }, TaskScheduler.FromCurrentSynchronizationContext());
 
        }

Je test le statut de ma tâche; et en fonction de celui-ci, je maj de manière différente mon UI, de plus, j’affiche le statut de cette tâche. On obtient le résultat suivant:

image

ca marche pas! et pire encore, la tâche à le statut RanToCompletion (ie: j’ai fini sans problème coco) !!

…Tout ca pour dire que ca n’est pas aussi simple que le laisse penser les API TPL:

  • il ne suffit pas de passer un CancellationToken et de faire cancel sur la source pour que cela fonctionne OOB.
  • si vous voulez le faire…vous ne ferez pas l’économie d’un tour sur la msdn Sourire.
  • Si on souhaite que cela fonctionne, il éxiste plusieurs méthodes qui peuvent correspondre a des scénarios différents, je vais donc tenter de les présenter par la suite:

    Je vérifie à la fin de la tâche si celle-ci à été interrompue et je gère en fonction mon affichage:

    On test tout simplement l’objet CancellationToken au lieu du statut de la tâche:

           void CancelTest4()
            {
                listBox.Items.Add("start loading...");
                Task.Run(() =>
                {
                    System.Threading.Thread.Sleep(3000);
     
                }, cts.Token).ContinueWith(t =>
                {
                    if (cts.IsCancellationRequested)
                        listBox.Items.Add("finish...but canceled");
                    else if (t.IsCompleted)
                        listBox.Items.Add("finish");
                    else if (t.IsFaulted)
                        listBox.Items.Add("Error during loading");
     
                    listBox.Items.Add("Task status->" + t.Status);
                }, TaskScheduler.FromCurrentSynchronizationContext());
            }
    Ce qui nous donne :
     
    image
     
    Ca fonctionne mais la tâche s’est exécuté complètement. C’est bien lorsque l’on a des tâches de traitement qui ne sont pas “gênantes” pour l’utilisateur comme les traitements “court” et qui ne consomment pas trop de resources,…
     

    On en fait le moins possible et on s’attend à une exception qui interromps le traitement: utilisation  Task.Wait/WaitAll/WaitAny :

        async void CancelTest3()
            {
                try
                {
                    listBox.Items.Add("start loading...");
                    var status = await Task.Run(() =>
                     {
     
                         var t = Task.Run(() =>
                          {
                              System.Threading.Thread.Sleep(3000);
                          }, cts.Token);
                         t.Wait(cts.Token);
                         return t.Status;
                     });
                    listBox.Items.Add("Finish");
                    listBox.Items.Add("Task status->" + status);
                }
                catch (Exception e)
                {
                    listBox.Items.Add(e.Message);
                }
            }

    Dans cette exemple, j’utilises async/await pour éviter d’avoir a lancer deux tâches (une pour annuler la première) et complexifier mon exemple: sans cela, je ne peut pas cliquer sur le bouton Cancel car mon UI est bloquée. Je lance une première tâche asynchrone qui lance la tâche qui simule le traitement. Derrière j’attend qu’elle se termine avec la méthode t.Wait().

    Si je fait mon test (lancer le chargement et cancel dans la foulée) j’obtient ca:

    image

    Je me mange une exception tout de suite après avoir cliquer sur le bouton Cancel, du coup, un petit try/catch me permet de gerer simplement la stratégie de MAJ de mon UI en cas d’annulation de ma tâche.

    Ca marche, c’est un peu crade pour cet exemple mais c’est très utile si par exemple vous lancez  plusieurs tâches de chargement (BDD, profil utilisateur,…) et que vous souhaitez les annuler simplement.

     

  • Je gère dans mon traitement de chargement l’interruption de la tâche

  • AU final, je pense que c’est le cas que l’on doit le plus privilégier. Dans la méthode de chargement, je sait comment m’interrompre “proprement” et faire remonter soit une exception, soit un statut “propre” à l’appelant:

    Sans exception:

     void CancelTest5()
            {
                listBox.Items.Add("start loading...");
                CancellationToken token = cts.Token;
                Task<string>.Run( () =>
                {
                    string s = "step1";
                    System.Threading.Thread.Sleep(3000);
                    s = "step2";
                    if (!token.IsCancellationRequested)
                    {
                        System.Threading.Thread.Sleep(1000);
                        s = "step3";
                    }
                    return s;
                }, cts.Token).ContinueWith(t =>
                {
                    
                    listBox.Items.Add(t.Result);
     
                    listBox.Items.Add("Task status->" + t.Status);
                }, TaskScheduler.FromCurrentSynchronizationContext());
     
            }

    Ici, on à découper le traitement de chargement en plusieurs étapes (step1, step2,…)et on test au fur et a mesure (via la propriété IsCancellationRequested) si la tâche est annulé et on réagit en conséquence (dans notre cas, on ne fait pas la 3e étape). On obtient le résultat suivant:

    image

    Ca marche (mais on a tjr le statut de la tâche à RunToCompletion Triste).

       void CancelTest6()
            {
                listBox.Items.Add("start loading...");
                CancellationToken token = cts.Token;
     
               
                Task<string>.Run(() =>
                {
                    string s = "step1";
                    System.Threading.Thread.Sleep(3000);
                    token.ThrowIfCancellationRequested();
                    s = "step2";
                    token.ThrowIfCancellationRequested();
                    System.Threading.Thread.Sleep(1000);
                    s = "step3";
                    
                    return s;
                }, cts.Token).ContinueWith(t =>
                {
     
                    listBox.Items.Add(t.Result);
     
                    listBox.Items.Add("Task status->" + t.Status);
                }, TaskScheduler.FromCurrentSynchronizationContext());
     
            }

    On va juste utiliser la méthode ThrowIfCancellationRequested qui est liée au CancellationToken au lieu de tester la valeur de la propriété IsCancellationRequested.

    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: mardi 10 avril 2012 14:28 par fathi

    Commentaires

    Pas de commentaires

    Les commentaires anonymes sont désactivés

    Les 10 derniers blogs postés

    - Compte rendu : SharePoint / O365 : des pratiques pour une meilleure productivité par The Mit's Blog le 12-12-2014, 18:11

    - [TFS] Suppression des feature SQL Entreprise en masse par Blog de Jérémy Jeanson le 12-06-2014, 09:18

    - [Clean Code] règles de nommage par Fathi Bellahcene le 12-04-2014, 22:59

    - Windows To Go 10 et Upgrades impossibles par Blog de Jérémy Jeanson le 12-04-2014, 21:38

    - SharePoint OnPremise: Statistiques d’utilisation pour traquer les sites fantomes par Blog Technique de Romelard Fabrice le 12-03-2014, 10:28

    - SharePoint 2007: Script PowerShell permettant le backup de toutes les collections de sites d’une application Web par Blog Technique de Romelard Fabrice le 12-02-2014, 10:00

    - Xamarin : un choix précieux par .net is good... C# is better ;) le 12-01-2014, 15:10

    - Office 365: Comparaison des composants pour préparer votre migration de SharePoint 2007 vers Office 365 par Blog Technique de Romelard Fabrice le 11-28-2014, 16:20

    - 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