Démarrons avec les Tasks

Tags: Thread

Que vous le vouliez ou non, le développement multi-tâche est maintenant une obligation pour toute nouvelle application. Il est donc vital d’en comprendre les mécanismes et de s’y mettre le plus tôt possible.

En attendant le .NET Framework 4.5 avec les nouveaux mots clés async et await (qui seront disponibles pour C# et VB .NET), le développement d’applications multi-thread a déjà été largement simplifié avec l’apparition de l’espace de noms System.Threading.Tasks.

Issu de travaux de Microsoft Research, Parallel FX et PLINQ ont été implémentés au coeur du framework puisque ces classes se retrouvent dans mscorlib (montrant ainsi l’importance du développement multi-tâche).

Nous allons voir dans ce premier article un premier type d’utilisation des Tasks.

Création d’une Task

Une Task est un objet qui va nous permettre d’exécuter une méthode dans un nouveau thread.

Si la méthode est une action (ie ne retourne rien), c’est la classe Task, si la méthode est une fonction (Func) et qu’elle retourne un objet de type TResult, c’est la classe générique Task<TResult> qui est utilisée.

Il y a plusieurs façons de créer une Task. La première est de tout faire “à la main”, c’est-à-dire en créant nous même l’instance de la classe, en définissant l’action ou la fonction associée, à lancer l’exécution asynchrone et à attendre le résultat. Exemple :

Console.WriteLine("1/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
var task = new Task(() =>
{
    Thread.Sleep(500);
    Console.WriteLine("2/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
});

Console.WriteLine("3/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
task.Start();

Console.WriteLine("4/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
task.Wait();
Console.WriteLine("5/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);

 

Ce qui donnera à la sortie :

1/ ThreadId=9
3/ ThreadId=9
4/ ThreadId=9
2/ ThreadId=10 // dans un autre Thread
5/ ThreadId=9

Je sais, ce n’est pas très lisible puisque le code 2/ qui est placé avant 3/ et 4/ est exécuté après (async/await simplifieront la lecture), mais on voit que le code de la Task est exécutée dès qu’on exécute la méthode Start et on attend son résultat grâce à Wait. (vous pouvez d’ailleurs définir le temps d’attente maximal avec Wait : Wait(TimeSpan duree)).

Une autre façon de faire (que je préfère mais cela n’engage que moi), est d’utiliser la classe TaskFactory (et son générique TaskFactory<TResult>). Cette classe possède la méthode StartNew qui fait tout le travail pour vous de création de la Task et vous retourne cette Task. Un singleton de cette classe est même accessible grâce à la propriété static de Task : Factory. On peut donc écrire :

Console.WriteLine("1/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
var task = Task.Factory.StartNew(
    (() =>
         {
             Thread.Sleep(500);
             Console.WriteLine("2/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
         }));

Console.WriteLine("3/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);

task.Wait();
Console.WriteLine("4/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);

Vous allez me dire, cela fait exactement la même chose et ce n’est pas plus lisible. Mais comme StartNew retourne la Task créée, on a accès à sa méthode ContinueWith qui est exécutée à la fin de l’action (ou de la fonction) de la Task. On peut très bien simplifier le code de la façon suivante :

Console.WriteLine("1/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
Task.Factory
    .StartNew(
        (() =>
             {
                 Thread.Sleep(500);
                 Console.WriteLine("2/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
             }))
    .ContinueWith(
        task =>
            {
                Console.WriteLine("4/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
            });

Console.WriteLine("3/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);

 

Ce qui donne :

1/ ThreadId=10
3/ ThreadId=10
2/ ThreadId=11 // dans un autre thread
4/ ThreadId=12 // encore un autre thread

On a donc bien l’impression d’une continuité du code.

Interaction avec l’UI

Imaginons maintenant que l’on souhaite effectuer une tâche dans un autre thread et afficher son résultat dans un textblock de notre application WPF.

J’ai donc une simple Windows WPF avec un bouton et un textblock :

<StackPanel>
    <Button Content="Clic" Click="Button_Click" />
    <TextBlock x:Name="infos"/>
</StackPanel>

 

Et au clic sur le bouton, je lance une tâche qui me retourne l’heure :

private void Button_Click(object sender, RoutedEventArgs e)
{
    infos.Text = "Exécution en cours…";

    Console.WriteLine("1/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);


    Task<string>.Factory
        .StartNew(
            () =>
                {
                    Thread.Sleep(500);
                    Console.WriteLine("2/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
                    return DateTime.Now.ToString();
                })
        .ContinueWith(
            task =>
                {
                    Console.WriteLine("3/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
                    infos.Text = task.Result;
                });

    Console.WriteLine("4/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
}

 

Et là : C’EST LE DRAME !

J’ai une exception, InvalidOperationException, m’indiquant que je n’ai pas le droit de modifier mon UI dans un autre thread que le thread principal, le thread de l’UI.

C’est tout simplement, comme nous l’avons vu plus haut, que le code exécuté dans ContinueWith n’est pas le code principal. Il faut se resynchroniser avec le thread principal.

Heureusement, Task nous permet de le faire simplement, grâce à une surcharge de ContinueWith :

private void Button_Click(object sender, RoutedEventArgs e)
{
    infos.Text = "Exécution en cours…";

    Console.WriteLine("1/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);


    Task<string>.Factory
        .StartNew(
            () =>
                {
                    Thread.Sleep(500);
                    Console.WriteLine("2/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
                    return DateTime.Now.ToString();
                })
        .ContinueWith(
            task =>
                {
                    Console.WriteLine("3/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
                    infos.Text = task.Result;
                },
            TaskScheduler.FromCurrentSynchronizationContext());

    Console.WriteLine("4/ ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
}

 

Le TaskScheduler permet cette re-synchronisation. Simple non ?

NB : A la place de “Execution en cours…” vous pouvez faire apparaitre une petite animation pour signifier que vous êtes en train de travailler. N’oubliez pas de la faire disparaitre par la suite.

NB Bis : nous verrons dans un prochain article comment gérer les exceptions qui peuvent se produire dans notre code asynchrone.

blog comments powered by Disqus