[Techdays 2011] : WPF, le multithreading et le futur de la programmation asynchrone

C’était également ma première participation aux Techdays ! J’ai assisté à diverses sessions, orientées en majorité sur du contenu technique relatif au framework .NET : accès aux fonctionnalités de windows seven en C#, développement d’interfaces réactives en WPF 4 et programmation asynchrone avec C# Async. Je vais donc vous faire un résumé de ces sessions qui ont vraiment été intéressantes.

Je ne décrirai pas ici la séance plénière (keynote) qui avait pour thème central le Cloud Computing car mon voisin était M. Poulin et il a déjà fait une fort belle description de ce moment, qui est toujours plutôt sympathique pour démarrer la journée. Je note la démonstration en HTML 5 : joli, mais qui en fait permettait surtout à l’orateur une fine comparaison avec Silverlight sur le temps de développement. Les outils du Cloud dont notamment Sql Azure et intégrant du Silverlight ont l’air vraiment ergonomiques et visuellement très agréables.

Il s’agissait plus ou moins d’un amuse-bouche de ma journée. J’étais plutôt intrigué par le titre et en plus je ne connais pas bien windows 7 alors je suis allé écouter l’intervenant de Bewise, Sacha Leroux, Microsoft Regional Director (labs.bewise.fr). Et en effet, qui dit nouvel OS, dit nouvelle problématique d’exposition des API. Le point central de l’exposé qui en découle était donc la présentation du Windows API Code Pack (pour le framework .Net, à partir du 2.0), qui est donc une librairie permettant d’accéder à certaines fonctionnalités de windows 7 et vista à partir de code managé, fonctionnalités qui ne sont donc pas accessibles dans le framework .NET de base :

  • Manipulation de la windows Taskbar et des sous éléments associés : les fameuses Jump Lists, Icon Overlay, Progress Bar, Thumbnail toolbars.
  • Utilisation d’aero
  • Power Management
  • Windows Shell
  • DirectX, Direct2D et Directwrite

http://code.msdn.microsoft.com/WindowsAPICodePack pour la liste complète !

Une démonstration a été réalisée : la conception d’un ‘Paint’ qui n’avait bien sûr aucune prétention de ressembler à l’original mais voulait illustrer l’utilisation de la librairie Direct2D (successeur de GDI/GDI+, image vectorielle, anti aliasing, profite de l’accélération matérielle).

Enfin le dernier exemple montrait la ‘Sensor Platform API’, qui permet d’avoir accès aux capteurs de windows (http://blog.tekigo.com/post/2009/06/29/Windows-7-une-API-specialisee-pour-les-capteurs.aspx pour plus d’informations) : sur l’écran, un modèle 3D d’un salon virtuel et la lumière dans ce salon virtuel était couplée à la lumière qui arrivait sur un circuit intégré avec capteur de luminosité branché sur son portable en USB. En mettant la main au dessus du capteur, le salon virtuel tombait dans l’obscurité.

Bref, des exemples plutôt sympa, même si, dans certain cas, il est évident qu’on peut faire aussi bien plus simplement en WPF. WPF 4 doit d’ailleurs intégrer les fonctionnalités de la taskbar. Mais cette API est compatible 2.0 donc peut quand même se révéler utile !

Après une petite interruption du midi, on rentre dans le vif d’un sujet qui m’intéressait tout particulièrement : des bonnes pratiques pour le développement des interfaces WPF ! Présentée par David Catuhe (Bewise) et un intervenant de Microsoft, Eric Vernié, qu’on avait pu apercevoir lors de la plénière, cette session met donc l’accent sur une problématique classique des IHMs : comment laisser l’interface réactive alors que dans le même temps on souhaite réaliser des traitements métiers lourds. L’exemple le plus classique étant ces fameux « application/window not responding » qu’on peut parfois voir dans nos applications alors qu’on vient de cliquer sur un bouton. Des fois on finit par reprendre la main… et des fois non. Ceci découle du fonctionnement du moteur de windows de gestion des fenêtres : en effet sous windows une fenêtre a une boucle de message qui tourne sous le même thread que le contrôle de l’UI (UI WPF par exemple). Si ce thread qu’on appelle ‘principal’ est surchargé, on a la fameuse fenêtre grisée.

On va donc voir quelles sont les classes qui vont nous permettre de faire des traitements métiers en arrière plan tout en n’essayant tant que possible de ne pas bloquer l’interface.

Précisons la chose pour WPF : une application démarre avec deux threads : le rendering Thread et l’UI Thread. Les objets graphiques ne peuvent être manipulés que par l’UI thread, cela vient de ce qu’on appelle le Thread Affinity (on ne peut manipuler un objet que dans le thread dans lequel il a été crée c’est d’ailleurs assez explicite dans les exceptions que tout développeur a du recevoir au moins une fois lors de développement WPF… si vous n’avez pas reçu d’exception compréhensible mais un vague message indicant un probleme STA… et bien ce STA veut tout simplement dire ‘Single Thread Affinity’). Les explications complètes sur http://msdn.microsoft.com/en-us/magazine/cc163328.aspx et il y a même une vraie version française puisqu’il s’agissait du magazine : http://msdn.microsoft.com/fr-fr/magazine/cc163328.aspx.

Donc, notre point d’accès à l’UI thread sera la classe Dispatcher, c’est une boucle de messages qu’on peut manipuler pour faire respirer l’application.


// The Work to perform on another thread

ThreadStart start = delegate()

{

// Sets the Text on a TextBlock Control.

// This will work as its using the dispatcher

Dispatcher.Invoke(DispatcherPriority.Normal,

new Action<string>(SetStatus),

"From Other Thread");

};

// Create the thread and kick it started!

new Thread(start).Start();

En fait l’idéal dans notre idée d’avoir toujours l’UI réactive est de l’updater de manière asynchrone :

// Work to perform on another thread

ThreadStart start = delegate()

{

// This will work as its using the dispatcher

DispatcherOperation op = Dispatcher.BeginInvoke(

DispatcherPriority.Normal,

new Action<string>(SetStatus),

"From Other Thread (Async)");

DispatcherOperationStatus status = op.Status;

while (status != DispatcherOperationStatus.Completed) {

status = op.Wait(TimeSpan.FromMilliseconds(1000));

if (status == DispatcherOperationStatus.Aborted)

{ // Alert Someone }

}

};

// Create the thread and kick it started!

new Thread(start).Start();

En fait on se rend compte que ce comportement attendu n’est pas inconnu aux développeurs windows forms : c’est la classe BackgroundWorker, réutilisable en WPF, et qui encapsule un travail asynchrone :


BackgroundWorker _backgroundWorker = new BackgroundWorker();

// Set up the Background Worker Events

_backgroundWorker.DoWork += _backgroundWorker_DoWork;

_backgroundWorker.RunWorkerCompleted += _backgroundWorker_RunWorkerCompleted;

// Run the Background Worker

_backgroundWorker.RunWorkerAsync(5000);

// ....

// Worker Method

void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e)

{ // Do something }

// Completed Method

void _backgroundWorker_RunWorkerCompleted(

object sender,

RunWorkerCompletedEventArgs e) {

if (e.Cancelled)

{ statusText.Text = "Cancelled"; }

else if (e.Error != null)

{ statusText.Text = "Exception Thrown"; }

else

{ statusText.Text = "Completed"; }

}

Pour ceux qui veulent fouiller dans le fonctionnement de tout ceci, tout est en fait basé sur la classe SynchronizationContext dérivée pour windows form (WindowsFormsSynchronizationContext), pour WPF (DispatcherSynchronizationContext) et même ASP .NET (AspNetSynchronizationContext). Cette classe particulière permet de traiter et synchroniser les appels cross-thread. Et elle est surtout disponible depuis le framework .NET 2.0 ce qui peut être intéressant pour coder une .dll séparée par exemple.

Le troisième élément présenté est le DispatcherTimer qui sert a l’exécution périodique de code, lui aussi utilise le dispatcher (on peut donc spécifier la priorité) et permet donc d’accéder à l’UI pendant son travail.

// Create a Timer with a Normal Priority

_timer = new DispatcherTimer();

// Set the Interval to 2 seconds

_timer.Interval = TimeSpan.FromMilliseconds(2000);

// Set the callback to just show the time ticking away

// note : since we are using a control, this has to run on the UI thread

_timer.Tick += new EventHandler(delegate(object s, EventArgs a)

{

statusText.Text = string.Format(

"Timer Ticked:  {0}ms", Environment.TickCount);

});

// Start the timer

_timer.Start(); 

Un quatrième outil pour le multithreading : ThreadPool et sa method QueueUserWorkItem.


private void Expand(ItemCollection itemsCollection) {

foreach (object item in itemsCollection)

{

var local = item;

Dispatcher.Invoke(DispatcherPriority.SystemIdle, new Action(() =>

{ (treeView.ItemContainerGenerator.ContainerFromItem(local) as TreeViewItem).IsExpanded = true; }));

}

Dispatcher.Invoke(new Action(() => {

progress.Visibility = Visibility.Collapsed;}));

}

private void Window_Loaded(object sender, RoutedEventArgs e) {

treeView.ItemsSource = entities.Blocks;

}

private void Button_Click(object sender, RoutedEventArgs e) {

progress.Visibility = Visibility.Visible;

ThreadPool.QueueUserWorkItem(o => Expand(treeView.Items));

}

Une petite explication : l’application en fil rouge de cette session était un chargement d’une base de données décrivant une collection de cartes à jouer. Ici on utilise le threadpool pour déplier tous les nœuds de premier niveau, on a beaucoup de données à charger, ce qui illustrait bien notre problème. Ca saccade encore un peu, mais l’interface n’est pas figée.

Le cinquième outil est la TPL ou Task Parallel Library : c’est un framework de programmation parallèle qui simplifie la gestion de la concurrence, de la synchronisation et de l’écriture de code parallèle pour exécution avec de multiples processeurs. La TPL expose donc des méthodes de traitement parallèle pour des instructions comme For et Foreach. Le travail d’instanciation et de fermeture des threads, ainsi que leur nombre (en fonction du nombre de processeur) est fait par la librairie. TPL inclut des classes importantes comme Task et Future. Une Task est une action qui peut être exécutée indépendamment du reste du programme (donc l’équivalent sémantique d’un thread) et géré par le Task Manager.

Toujours avec le fil rouge sur une problématique un peu différente : le chargement de wallpaper à partir d’un site internet et leur affichage dans une fenêtre WPF. On lance les téléchargements avec un Parallel.ForEach :

public static void Gather(string root)

{

context = SynchronizationContext.Current;

Task.Factory.StartNew(() =>{

string data = GetData(root, false);

Parallel.ForEach(Tools.GetPictureLinks(data), Process);

RaiseGatherFinished(); });

} 

Dans la methode Process  on fait un :

foreach (string wallpaper in finalWallpapers) {DownloadAndShow(wallpaper, finalWallpapers.Count, index++);} 

DownloadAndShow telecharge un wallpaper, l’écrit sur le disque… et pour ajouter l’objet wallpaper dans notre application WPF on a besoin d’accéder au thread originel, on utilise donc le contexte, qui est bien un SynchronizationContext que j’avais évoqué précédemment :

context.Post(state => {

Wallpapers.Add(new WallPaper { Filename = fileName, Small = smallFileName });

}, null);

Un autre exemple, toujours dans la méthode Process, on met à jour les progress bars avec RaiseTaskProgress :

static void RaiseTaskProgress(int progress)

{

int id = Task.CurrentId.Value;

context.Post(state =>

{

if (OnTaskProgress != null)

OnTaskProgress(id, progress);

}, null);

}

Et honnêtement, la démonstration avec le portable de David et ses 8 cœurs est carrément bluffante au niveau de la réactivité de l’interface.

Hormis la TPL, le framework fournit d’autres outils pour contrôler ses threads : Semaphore, Mutex. Il faut faire attention tout de même aux limites dans nos ressources : nombres de CPUs potentiels, nombre de connexions réseau parallèles et concurrence d’accès aux matériel. Pour illustrer ce point, sur l’application précédente de téléchargement d’images multiples, David nous montre en direct l’utilisation d’une instruction pour limiter le nombre de cœurs occupés à 4 : en effet, le site internet cible n’acceptait que 4 connections concurrentes, et cela se voyait sur l’interface WPF avec seulement 4 barres de progression en activité sur les 8 instanciées.

Pour conclure sur tous ces outils de multithreading, il ne faut pas oublier que toutes ces techniques reviennent à encapsuler des threads systèmes : on a toujours la nécessité de passer par le dispatcher pour atteindre l’UI, mais cela permet d’améliorer grandement les performances en ne faisant pas faire de traitements métiers lourds à l’interface.

Pour terminer, deux points intéressants concernant directement WPF :

–  Binding et parallélisme : utilisation de la propriété Binding.IsAsync, introduite par WPF 3.0.

Dans un premier temps, le binding retourne la valeur FallbackValue (ex : ‘not loaded yet’). De manière asynchrone sans bloquer l’UI thread, le binding va attendre le calcul de sa valeur. Là encore la démonstration sur le treeview de la collection d’images est intéressante : on avait un champ bindé ‘calculé’ qui était la concaténation complexe de trois autres champs : on expande le nœud et on voit bien que ce champ là est en ‘not loaded yet’ et se met à jour devant nos yeux au fur et à mesure.

– David nous a annoncé une future nouveauté de la prochaine mouture WPF : les observables collections ne seront plus liées au thread principal (ce qui paliera aux problèmes de STA déjà évoqués précédemment : typiquement  ‘This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.)

En tout cas, j’ai vraiment apprécié cette session avec deux intervenants motivés et sympathiques, et des exemples vraiment éclairés sur des problématiques ni trop simples ni trop complexes ce qui permet d’appréhender justement ces bonnes pratiques de conception d’interfaces graphiques réactives !

En outre, le matériel est déjà disponible sur le site de David Catuhe et toutes les applications passées en démonstration y figurent : http://www.catuhe.com/post/Les-demos-de-mes-sessions-des-TechDays-2011.aspx . Ne serait ce que pour voir par vous-même les différences de réactivité au fur à et à mesure qu’il raffine les techniques, ca vaut le coup.

Enfin si je n’ai pas parlé du point rapide qui a été fait sur l’utilisation de l’Async Framework, actuellement en CTP (Community Technology Preview, donc une version beta), c’est parce qu’on va pouvoir le découvrir dans le paragraphe suivant, puisque c’est la session que je suis allé voir juste après.

Pourquoi ‘Part II’ ? Car il y a eu une session précédente sur les Reactives Extensions (RX). Cf par exemple http://blogs.developpeur.org/raptorxp/default.aspx, un blog avec 3 articles sur RX.

Cette session est donc animée par Mitsu Furuta, connu pour avoir travaillé chez Microsoft pendant un certain temps et pour ses sessions ‘Coding 4 Fun’, et maintenant chez Sensor IT, accompagné de Christophe Nasarre, un intervenant Microsoft. Et ils vont nous parler d’une CTP, Community Technical Preview, une ‘beta’ qui devrait être releasé durant l’été de cette année, le Framework Async, qui répond à une problématique moderne : l’utilisation de l’asynchronisme comme on vient par exemple de le voir pour rendre nos interfaces plus réactives, redonner la main à l’utilisateur pendant les traitements en arrière plan, etc.

Bien sûr, depuis ses premières versions, le framework .Net a exposé plusieurs mécanismes asynchrones : les évènements, délégués, callbacks, timers, threads, task et autres Backgroundworker offrent différents moyens de faire des traitements asynchrones en branchant du code dont l’exécution sera différée, parallèle et/ou conditionnelle. Et il est vrai que certaines syntaxes ne sont ni forcément agréables à manipuler, ni lisibles dans le flux du code. La CTP Async de C# 5 est donc une approche tendant à unifier et simplifier la programmation asynchrone.

Et la session de commencer avec un exemple simple mais plutôt éclairé, celui de la machine à laver, qui est l’exemple même de ce qu’on évoque implicitement quand on parle de programmation, en fait la programmation séquentielle : des instructions exécutées à la suite des autres. Cependant les scénarios asynchrones sont en fait multiples dans le monde de l’informatique : entrées/sorties, filewatchers, interruptions, parallélisme (pas asynchrone par nature, mais du fait du non-déterminisme d’opérations parallèles, peut rendre une opération asynchrone).

Le problème du programmeur est que l’exécution ne respecte pas l’ordre de l’écriture, et qu’il n’est pas toujours évident de savoir ce qui va être executé et à quel moment quand de multiples opérations asynchrones se croisent. La CTP fournit de nouveaux mots clés, await et async, et une syntaxe pour les opérations asynchrones.

A noter que l’objet Task (qu’on a vu dans la session précédente) sera étendu pour gérer les opérations asynchrones par l’objet TaskEx (nécéssité d’avoir un nouveau nom d’objet car c’est la CTP, mais dans le futur Task, donc).

Un exemple fourni dans le whitepaper disponible sur le site officiel, http://msdn.microsoft.com/en-us/vstudio/async.aspx :

public async Task<int> SumPageSizesAsync(IList<Uri> uris) {

int total = 0;

foreach (var uri in uris) {

statusText.Text = string.Format("Found {0} bytes ...", total);

var data = await new WebClient().DownloadDataAsync(uri);

total += data.Length;

}

statusText.Text = string.Format("Found {0} bytes total", total);

return total;

}

L’exemple précédent requête des pages web en utilisant WebClient.DownloadDataAsync et additionne leurs tailles. La méthode est marquée comme async ce qui prépare le compilateur à modifier la méthode dès qu’il rencontrera le mot clé await : en arrivant à l’appel de la méthode asynchrone DownloadDataAsync, le compilateur génère lui même l’abonnement à l’évènement et le callback.
Toutes les instructions de la méthode suivant le mot clé await sont en fait déplacées dans le callback.

Par ailleurs, alors que classiquement on récupérait une valeur de retour avec un task.result, le prototype décrit maintenant la valeur de retour de la méthode comme Task et dans notre exemple c’est un entier qui est renvoyé. Le compilateur encapsule l’entier dans un objet Task grâce au mot clé async. Donc quand on écrit : var x= await(new Task<T>…), x est un T.

Attention, bien que le mot clé await nous fasse penser à un appel bloquant, ce n’est pas exactement le cas : await permet de lancer une tâche de manière asynchrone et de ressortir immédiatement du contexte de la méthode préfixée par async. Il est juste qu’on ne reviendra exécuter la ligne de code qui suit l’appel à la tâche uniquement lorsque celle-ci sera terminée mais dans le code appelant la methode async, on continue les instructions.

Un exemple de code tiré de http://blogs.msdn.com/b/stephe/archive/2011/01/25/diff-233-rences-entre-la-task-parallel-library-et-async-await-pour-les-nuls.aspx (bonne récapitulation des différences/combinaisons entre TPL et Async F.) ainsi que l’explication du résultat :

async void Button_Async(object sender, RoutedEventArgs e)

{

Trace1();

AsyncProcSeq();

Trace4();

}

void AsyncProcSeq()

{

Trace2();

await Task.Factory.StartNew(action1);

await Task.Factory.StartNew(action2);

await Task.Factory.StartNew(action3);

Trace3();

}

On obtient le résultat suivant à l’exécution:


1 >

2 >>

4 <

Fin Action1

Fin Action2

Fin Action3

00:00:06.0407339

3 <<

Le premier appel asynchrone ( await Task.Factory.StartNew(action1 ) ) rend immédiatement la main en ressortant du contexte de la méthode AsyncProcSeq, et en exécutant la suite des lignes de code de la méthode appelante, à savoir Trace4() (un peu comme avec yield). A la fin de l’exécution de action1, l’exécution asynchrone de l’action2 sera déclenchée, et il en sera de même ensuite pour action3 une fois que l’exécution de action2 sera terminée. Trace3() sera appelé à la fin de l’exécution des 3 actions, tout comme avec le code de continuation de tout à l’heure. Attention, par contre les 3 actions ne s’exécutent pas en parallèle ici, mais bien de manière séquentielle : elles ont mis 6 secondes à s’exécuter.

Le flot d’éxécution est néanmoins respecté : tout se déroule dans l’ordre où on l’écrit et le code est bien plus lisible. Attention pour ceux qui connaissent et utilisent l’objet Task actuel, les méthodes marquées async ne s’exécutent pas dans un nouveau thread elles utilisent le thread courant. Si vous voulez exécuter le code en dehors du thread courant vous devez explicitement utiliser Task.Run(). Signalons que l’objet TaskEx contient d’autres méthodes intéressantes comme  notamment WhenAll quipermet de lancer une série d’opérations asynchrones parallèlement et de savoir quand elles sont toutes terminées :

 var result = await TaskEx.WhenAll(ListeObjets.Select(o=> OperationAsync(o)); 

Quid de la gestion de l’annulation et des exceptions ? L’annulation est un cas particulier d’exception qui est géré par un token de synchronisation qui déclenche une exception lors d’une annulation. Et ici ce qui est encore vraiment intéressant grâce au compilateur : quand on écrit un try { await(new Task<T>….)} catch{ OperationCancelledException}, on récupère l’exception de la tache elle-même. Alors que traditionnellement, on ne pouvait pas récupérer l’exception de la tâche à l’extérieur d’elle-même.


private CancellationTokenSource cts;

public async Task AsyncCancelSingle()

{

cts = new CancellationTokenSource();

try

{ WriteLinePageTitle(await DownloadStringTaskSlowNetworkAsync(new Uri("http://www.google.fr"), cts.Token)); }

catch (OperationCanceledException)

{ Console.WriteLine("Downloading canceled."); }

}

En résumé, cette CTP apporte une nouvelle syntaxe qui permet de voir dans notre code un flot d’exécution séquentiel bien plus clair et étend les fonctionnalités intéressantes de l’objet Task.

Mitsu a conclu en nous parlant des Rx Extensions (la première session sur la programmation asynchrone) : Rx est un produit fini disponible sur de nombreuses plateformes dont les fonctionnalités sont proches de celles de la CTP, mais en fait les deux technologies devraient être utilisées de manière complémentaire, en particulier avec les vues Rx IObservables qu’on peut brancher sur les résultats de nos tâches asynchrones.

Conclusion

Le site des techdays http://www.mstechdays.fr indique que les webcasts et présentations de toutes les conférences devraient être disponibles aux alentours du 15 mars 2011. En ce qui me concerne, j’ai vraiment suivi avec intérêt ces sessions sur WPF, les exemples démontrés étant complets et pertinents . En outre il est toujours sympathique de voir ce que sera le futur des frameworks de développement que l’on utilise tous les jours : j’attends avec une véritable impatience de voir ce que nous réserve d’autre la prochaine mouture de C#.

Nombre de vue : 228

AJOUTER UN COMMENTAIRE