Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Thread vs Parallélisme

[new:30/03/2014]Beaucoup n’ont pas encore infléchi leur style de programmation vers le multitâche pourtant devenu indispensable. Certains l’ont fait et pensent que jouer avec les Threads est suffisant. En réalité le Threading n’est pas forcément équivalent à du parallélisme. Faisons le point !Plus...

Rx Extensions, TPL et Async CTP : L’asynchronisme arrive en parallèle !

[new: 15/12/2011]Les RX Extensions, TPL et Async CTP sont trois technologies releasées ou en cours de l’être, toutes les trois traitent d’asynchronisme et de parallélisme. Toutes les trois déboulent presque en même temps, ce qui est une belle illustration d’auto-référence ! Mais en dehors de ça, comment comprendre cette avalanche et que choisir ?Plus...

Parallel FX, P-Linq et maintenant les Reactive Extensions…

[new:10/8/2010]Les Parallel Extensions, connues jusqu’à lors sous le nom de Parallel Framework Extensions (ou PFX) forment une librairie permettant de faciliter la construction d’algorithmes parallèles (multi-thread) tirant partie des machines multi-cœur. Je vous en avais déjà parlé, ainsi que de P-Linq les extensions parallèles pour LINQ. Deux choses importantes à savoir aujourd’hui : les Parallel Extensions font partie de .NET 4 (VS 2010, Silverlight…) et une nouvelle librairie arrive, les Reactive Extensions !Plus...

Silverlight et le multi-thread (avec ou sans MVVM)

La “parallélisation” des traitements est devenu un enjeu majeur et paradoxal du développement et Silverlight n’échappe pas à ce nouveau paradigme.

Nouveau Paradigme ?

Nouveau paradigme, cela fait toujours un peu “attention je suis en train de dire quelque chose d’intelligent et j'utilise une tournure à la mode qui fait savante pour le faire remarquer”… Mais parfois cela a aussi un vrai sens et une vraie justification !

Nouveau paradigme donc que la programmation multitâche, ou pour être encore plus chébran, parlons de programmation parallèle, voire de parallélisme tout en évitant le trop prétentieux “massivement parallèle” (nos PC ne comptent pas encore la centaine de “cores” – plutôt que “cœurs” pour faire totalement geek – qu’Intel prévoit pour dans quelques temps).

On le sait depuis un moment, la fréquence des processeurs n’évoluera plus comme elle l’a fait jusqu’à lors. Le fondeur de processeurs se retrouve ainsi face à un mur infranchissable, un peu comme l’était le mur du son pour l’aviation jusqu’à ce jour d’octobre 47 ou le pilote Yeager à bord du Bell-X-1 finit par traverser les briques de ce mur qu’on avait cru impénétrable. Pour nous il s’agit du “mur des performances”. On miniaturise, on diminue l’épaisseur des couches, on tasse les transistors, mais on butte sur la fréquence. Le seul moyen d’aller plus vite reste de multiplier les processeurs… Et comme multiplier les cartes mères atteint vite une limite en termes de prix et de consommation électrique, on essaye désormais de tasser des cœurs sur les puces comme on tassait les transistors à l’étape précédente. Ce n’est pas une option et il n’y a pas de “plan B”, c’est la seule solution jusqu’à ce qu’existent les ordinateurs quantiques (dont la faisabilité reste totalement à prouver).

Deux, quatre, huit, trente-deux, bientôt cent cœurs dans les machines de monsieur Tout-Le-Monde. 

Hélas ce n’est pas avec les techniques de programmation utilisées jusqu’aujourd’hui que les informaticiens vont s’en sortir. Les fondeurs ont trouvé le moyen de percer le “mur des performances”, et si les informaticiens ont une idée de comment ils vont relever le défi, ils ne se sont pas encore dotés des outils permettant de le faire : une formation ad hoc et une approche radicalement différente de la programmation.

On peut ainsi réellement, et sans emphase exagérée, parler de “nouveau paradigme” de la programmation parallèle car seule cette nouvelle approche va permettre aux logiciels de bénéficier des accélérations offertes par les processeurs modernes et encore plus par ceux à venir. Et pour cela il va falloir que les développeurs se mettent à niveau alors qu’ils sont déjà à la traîne de cette révolution…

J’ai dernièrement acquis une machine à 4 cœurs hyperthreadés, donc à 8 cœurs apparents. Avec certaines applications elle va à peine aussi vite que mon double cœurs E6700 overclocké (et pas toujours)… Je regarde la jolie fenêtre des performances de Windows, je vois ces huit magnifiques graphiques qui sont autant de preuves formelles de la puissance de la bête (et un enchantement de geek), mais un seul s’active vraiment, un autre le faisant mollement, et les 6 autres piquant un roupillon désespérant ! Ces applications je les maudis et dans la foulée leurs concepteurs incompétents qui ont programmé comme on le faisait il y a 20 ans : en pensant qu’un seul processeur anime la machine et que Windows va se débrouiller pour partager le temps (et que si l’utilisateur trouve que “ça rame” il n’a qu’à acheter une machine récente…).

En revanche je suis plein de compliments confraternels à l’égard des développeurs qui ont programmé les quelques applications qui savent se servir de mes huit cœurs ! Ces programmes qui s’essoufflaient sur le E6700 deviennent des bombes sur le i870, ou, au minimum de mes attentes, vont nettement plus vite, ce qui n’est déjà pas si mal.

Faire partie de ceux qu’on maudit ou de ceux qu’on félicite, il va falloir choisir son camp !

Programmer “parallèle” devient ainsi une obligation de fait, techniquement parlant. Mais il ne faut pas ignorer non plus les cas plus classiques où le parallélisme est le seul moyen de programmer certaines fonctions. C’est ce que nous verrons dans un instant au travers d’un exemple de code.

Mais paradoxal !

En effet, ce nouveau paradigme n’en est pas moins paradoxal . La raison est simple : d’un côté les multi-cœurs se généralisent dans les PC et de l’autre l’informatique mobile explose avec des téléphones aux interfaces graphiques à faire pâlir les progiciels PC les plus chers, des iPad, des EEEPC, et autres merveilleuses petites machines ayant toutes en commun d’être dotées d’un seul processeur rikiki et monocœur !

Alors que .NET 4.0 arrive avec des dizaines de méga de couches plus sophistiquées les unes que les autres, simultanément Silverlight et son mini Framework sont annoncés (avec XNA) comme l’interface des Phones 7, voire bientôt de certaines tablettes PC…

Faire le grand écart entre ces deux tendances extrêmes et néanmoins concomitantes relève du tour de force et place l’informaticien dans une situation paradoxale. Et pourtant ce sont deux défis que le développeur doit dès aujourd’hui relever, et simultanément ! Même en s’y mettant tout de suite, il part en retard (les “utilisateurs de base” possèdent depuis plusieurs années des machines à double cœurs !). Il faut donc programmer en visant la “scalability” ou “capacité à s’adapter”, s’adapter à la charge, s’adapter au matériel. Tirer le meilleur d’un Phone 7, faire cracher ses chevaux à un PC de bureau 8 cœurs. En gros il faut maîtriser la programmation parallèle.

En pratique on fait comment ?

On sort ses neurones du mode économie d’énergie certifié Energy Star et on se sert de l’ordinateur le plus massivement parallèle de l’univers : son cerveau :-)

Cela signifie adopter une nouvelle “attitude” face à la programmation quotidienne. La moindre classe doit être pensée dans un contexte multi-tâche. Cela doit devenir un automatisme, comme de finir chaque instruction par un point-virgule en C#.

Mais comme il n’est pas question dans ce blog de faire un cours complet sur le multi-tâche, attardons-nous sur un exemple concret et voyons comment introduire un peu de ces réflexes qui tendent vers une meilleur utilisation des ressources physiques sous-jacentes offertes par la machine hôte de nos chefs-d’œuvres compilés….

Un exemple concret

programmer en vue d’une utilisation multi-tâche n’est pas forcément simple. On peut être ainsi tenter d’y échapper jusqu’au jours où on tombe sur un cas tout simple qui va déboucher sur un magnifique plantage.

Comme le titre du billet l’indique, cela n’a rien de spécifique à MVVM, même si l’exemple sera programmé dans un mode de ce type (mais très édulcoré, ce n’est donc pas un exemple MVVM, pour cela se reporter à l’article complet que j’ai écrit sur le sujet).

Imaginons ainsi une vue présentant des données issues d’un ViewModel ou directement d’un Modèle (ce que MVVM autorise). Supposons ce dernier cas pour simplifier la “plomberie” de l’exemple. Ce Modèle simule une source de données un peu particulière puisqu’elle fonctionne en permanence et dépend de données externes qui arrivent un peu quand elles veulent. Cela n’a rien d’exotique, prenez un simple Chat, il y aura bien un Modèle fournissant les messages de l’interlocuteur fonctionnant en permanence et la fréquence d’arrivée des données (les messages) est aléatoire (quand l’interlocuteur a envi d’écrire).

Le modèle

Le Modèle de notre exemple sera bien plus simple encore : il fabrique des nombres aléatoires. Mais il les fabrique dans un thread séparé qui s’endort pour une durée aléatoire après chaque génération de nombre. Pour ce faire la classe RandomNumberPusher utilise un BackgroundWorker, classe du Framework permettant assez simplement de créer des tâches de fond (code source du projet à télécharger plus bas).

la Vue

Créons une vue (la MainPage sera suffisante pour cet exemple minimaliste) et attachons une instance de la classe RandomNumberPusher au DataContext de la grille principale. Ajoutons un TextBlock bindé (voir mon article sur le Binding Xaml) à la propriété Number. Et exécutons…

Le plantage…

A intervalle irrégulier l’instance du Modèle va générer en tâche de fond un nombre aléatoire et va déclencher la notification de changement d’état de la propriété Number (événement PropertyChanged). La Vue et son TextBlock étant bindés à cette dernière, la mise à jour du texte va se faire automatiquement. Oui mais… cela va planter. Pour une raison simple : seule le thread principal ayant créé les objets d’interface peut modifier ces derniers, or le générateur aléatoire fonctionne depuis un thread différent. On peut mettre le plantage encore plus en évidence si on tentait de modifier directement la propriété Text du TextBlock depuis la tâche de fond. Le modèle de programmation MVVM nous oblige à penser le code autrement (et c’est une bonne chose) et le plantage ne se verra pas forcément (le TextBlock ne changera pas de valeur). Mais à un moment donné il risque de s’afficher ceci :

Accès inter-thread non valide. Le couperet tombe.

Pour obtenir ce message de façon sûre il faut placer un point d’arrêt dans le thread de fond, s’arrêter dessus et repartir car le plus souvent le texte n’est tout simplement pas mis à jour. Comme je le disais plus haut, le plantage serait systématique si la tâche de fond mettait à jour directement le TextBlock ce que le montage de type MVVM évite. Le bug n’en est que plus sournois car il ne se passe rien, et rien, ce n’est pas grand chose ! Sur un écran un peu chargé il sera difficile de détecter le problème durant les tests.

Le mécanisme est simple :

Le thread de fond modifie la propriété Number, ce qui déclenche l’événement de changement de propriété (PropertyChanged), l’objet de binding est alors activé et tente de mettre à jour la valeur de la propriété Text du TextBlock. La plantage résulte du fait que la modification de ce dernier est effectué au travers d’un thread différent de celui gérant l’UI.

Le code xaml

   1:  <UserControl x:Class="SLMultiThread.MainPage"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
   5:      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
   6:      xmlns:model="clr-namespace:SLMultiThread.Models"
   7:      mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
   8:      <UserControl.Resources>
   9:          <model:RandomNumberPusher x:Key="random" />
  10:      </UserControl.Resources>
  11:      <Grid x:Name="LayoutRoot" DataContext="{StaticResource random}">
  12:          <TextBlock Text="{Binding Number, Mode=OneWay}"/>
  13:      </Grid>
  14:  </UserControl>

On note : la déclaration du namespace de notre Modèle, la déclaration d’une instance du Modèle en ressource, la définition du DataContext du LayoutRoot depuis cette dernière et enfin, le TextBlock bindé à la propriété Number.

Le code C# de la classe RandomNumberPusher n’est pas en cause dans le mécanisme du plantage. Elle est programmée en répondant aux exigences d’une classe dite Thread-Safe. Donc pour l’instant oublions là.

Les solutions

Plusieurs aspects sont à prendre en compte lors d’une programmation multi-thread. Le plantage que nous venons de mettre en évidence n’est qu’un aspect des choses, limité uniquement à la spécificité de Xaml qui ne prend pas en charge la mise à jour de l’UI via un thread autre que le principal. On trouvait déjà ce problème sous Windows Forms d’ailleurs.

Concernant ce problème spécifique il y a plusieurs approches, mais au final il n’y a qu’un moyen : faire en sorte que la manipulation de l’UI ne passe que par son thread propre. Ponctuellement on peut utiliser, comme sous Windows Forms, des mécanismes d’invocation transférant le contrôle au thread de l’UI. Mais ce que nous cherchons est une solution globale et “industrialisable”. La plupart des auteurs qui a abordé le sujet arrive à la même conclusion, il faut passer par un mécanisme centralisé de dispatch. Cette solution peut se mettre en œuvre de différentes façons, voyons directement celle qui m’apparait la plus simple à appliquer.

Le SmartDispatcher et la classe de base

Le principe peut se schématiser comme suit :

Au début les choses sont, par force, identiques : la tâche de fond modifie la propriété (Number dans notre exemple) et déclenche PropertyChanged dans le Modèle. Mais au lieu que cet événement soit traité directement par le binding, nous intercalons le Dispatcher, c’est lui qui va vérifier que l’appel a lieu depuis le thread d’UI. Dans l’affirmative le traitement se poursuivra normalement, dans la négative, ce qui est le cas de notre exemple, il transfèrera la notification de changement de propriété au binding via une invocation appropriée. Comme le Dispatcher se trouve dans le thread de l’UI, et qu’il va faire en sorte de rendre “sien” les appels qui proviennent d’autres threads, le binding ne verra que des appels intra-thread UI et non inter-threads.

Cette solution fonctionne en deux étapes. Il y a le Dispatcher, mais il y a aussi le routage des notifications vers ce dispatcher. Modifier dans chaque classe l’appel à PropertyChanged pour qu’il pointe vers le Dispatcher serait fastidieux et source d’erreur. Le plus simple est donc de créer une classe de base dont hériterons les Modèles ou les ViewModels plus généralement (la connexion directe d’une Vue à un Modèle utilisée dans l’exemple est certes autorisé par MVVM mais ce n’est pas le “montage” le plus courant, le ViewModel étant souvent nécessaire pour la prise en charge des commandes, l’adaptation des données et la persistance de la Vue).

Le mécanisme du Binding et des patterns comme MVVM reposent sur un présupposé : que les classes reliées par binding à l’UI prennent en charge INotifyPropertyChanged. Il est donc raisonnable de mettre en œuvre une classe de base implémentant cette interface et s’occupant automatiquement de la liaison avec le Dispatcher.

Le seul problème que peut poser cette approche est que le ViewModel soit déjà dans l’obligation d’hériter d’une classe de base. Cela peut être le cas si vous utilisez un framework MVVM ou autre. C# ne gérant pas l’héritage multiple (bien heureusement), il faudra alors fusionner la solution du Dispatcher à la classe de base déjà utilisée. A moins que celle-ci ne prenne déjà en charge un mécanisme équivalent, mais dans ce cas je peux aller me coucher vous n’avez plus besoin de mes conseils ! :-)

Le code du Dispatcher

Parmi les implémentations existantes j’ai une préférence pour celle de Jeff Wilcox qui est un Senior Software Development Engineer chez Microsoft dans l’équipe de Silverlight… Autant se servir aux bonnes sources ! Son implémentation est sous licence Ms-PL (licence libre publique spécifique à Microsoft). J’avais dans mes tablettes un code assez proche mais pas aussi complet et on peut trouver des choses assez équivalentes dans le portage de Cairngorm sur Silverlight (Cairngorm sur CodePlex) , un framework MVC traduit depuis l’original de même nom conçu pour Flex de Adobe (un produit proche de Silverlight pour ceux qui ne connaissent pas).

   1:  using System;
   2:  using System.Windows;
   3:  using System.ComponentModel;
   4:  using System.Windows.Threading;
   5:   
   6:  namespace SLMultiThread.ThreadTools
   7:  {
   8:      // (c) Copyright Microsoft Corporation.
   9:      // This source is subject to the Microsoft Public License (Ms-PL).
  10:      // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
  11:      // All other rights reserved.
  12:      
  13:      
  14:          /// <summary>
  15:          /// A smart dispatcher system for routing actions to the user interface
  16:          /// thread.
  17:          /// </summary>
  18:          public static class SmartDispatcher
  19:          {
  20:              /// <summary>
  21:              /// A single Dispatcher instance to marshall actions to the user
  22:              /// interface thread.
  23:              /// </summary>
  24:              private static Dispatcher instance;
  25:   
  26:              /// <summary>
  27:              /// Backing field for a value indicating whether this is a design-time
  28:              /// environment.
  29:              /// </summary>
  30:              private static bool? designer;
  31:   
  32:              /// <summary>
  33:              /// Requires an instance and attempts to find a Dispatcher if one has
  34:              /// not yet been set.
  35:              /// </summary>
  36:              private static void requireInstance()
  37:              {
  38:                  if (designer == null)
  39:                  {
  40:                      designer = DesignerProperties.IsInDesignTool;
  41:                  }
  42:   
  43:                  // Design-time is more of a no-op, won't be able to resolve the
  44:                  // dispatcher if it isn't already set in these situations.
  45:                  if (designer == true)
  46:                  {
  47:                      return;
  48:                  }
  49:   
  50:                  // Attempt to use the RootVisual of the plugin to retrieve a
  51:                  // dispatcher instance. This call will only succeed if the current
  52:                  // thread is the UI thread.
  53:                  try
  54:                  {
  55:                      instance = Application.Current.RootVisual.Dispatcher;
  56:                  }
  57:                  catch (Exception e)
  58:                  {
  59:                      throw new InvalidOperationException("The first time SmartDispatcher is used must be from a user interface thread. Consider having the application call Initialize, with or without an instance.", e);
  60:                  }
  61:   
  62:                  if (instance == null)
  63:                  {
  64:                      throw new InvalidOperationException("Unable to find a suitable Dispatcher instance.");
  65:                  }
  66:              }
  67:   
  68:              /// <summary>
  69:              /// Initializes the <see cref="SmartDispatcher"/> system, attempting to use the
  70:              /// RootVisual of the plugin to retrieve a Dispatcher instance.
  71:              /// </summary>
  72:              public static void Initialize()
  73:              {
  74:                  if (instance == null)
  75:                  {
  76:                      requireInstance();
  77:                  }
  78:              }
  79:   
  80:              /// <summary>
  81:              /// Initializes the <see cref="SmartDispatcher"/> system with the dispatcher
  82:              /// instance.
  83:              /// </summary>
  84:              /// <param name="dispatcher">The dispatcher instance.</param>
  85:              public static void Initialize(Dispatcher dispatcher)
  86:              {
  87:                  if (dispatcher == null)
  88:                  {
  89:                      throw new ArgumentNullException("dispatcher");
  90:                  }
  91:   
  92:                  instance = dispatcher;
  93:   
  94:                  if (designer == null)
  95:                  {
  96:                      designer = DesignerProperties.IsInDesignTool;
  97:                  }
  98:              }
  99:   
 100:              /// <summary>
 101:              ///
 102:              /// </summary>
 103:              /// <returns></returns>
 104:              public static bool CheckAccess()
 105:              {
 106:                  if (instance == null)
 107:                  {
 108:                      requireInstance();
 109:                  }
 110:   
 111:                  return instance.CheckAccess();
 112:              }
 113:   
 114:              /// <summary>
 115:              /// Executes the specified delegate asynchronously on the user interface
 116:              /// thread. If the current thread is the user interface thread, the
 117:              /// dispatcher if not used and the operation happens immediately.
 118:              /// </summary>
 119:              /// <param name="a">A delegate to a method that takes no arguments and
 120:              /// does not return a value, which is either pushed onto the Dispatcher
 121:              /// event queue or immediately run, depending on the current thread.</param>
 122:              public static void BeginInvoke(Action a)
 123:              {
 124:                  if (instance == null)
 125:                  {
 126:                      requireInstance();
 127:                  }
 128:   
 129:                  // If the current thread is the user interface thread, skip the
 130:                  // dispatcher and directly invoke the Action.
 131:                  if (instance.CheckAccess() || designer == true)
 132:                  {
 133:                      a();
 134:                  }
 135:                  else
 136:                  {
 137:                      instance.BeginInvoke(a);
 138:                  }
 139:              }
 140:          }
 141:  }

Le SmartDispatcher est une enveloppe intelligente autour de la classe Dispatcher du Framework de Silverlight. Cette dernière a été conçue justement pour gérer le code de mise à jour de l’UI depuis d’autres threads. SmartDispatcher “habille” le Dispatcher de base en s’assurant qu’une instance est disponible. En fait, appeler le Dispatcher de SL serait suffisant, il est conçu pour cela, mais cela n’est pas possible depuis une tâche de fond, il faut donc que l’instance existe déjà. Le SmartDispatcher permet de s’assurer de cette condition liminaire et fournit un point d’accès connu et centralisé à l’instance en question.

Bien que SmartDispatcher s’occupe d’obtenir l’instance du Dispatcher automatiquement il est préférable de l’initialiser très tôt dans le code de l’application, pour s’assurer que le SmartDispatcher est bien opérationnel et disponible avant tout événement impliquant l’interface. C’est pourquoi il est préférable d’ajouter dans App.xaml le code suivant dans le constructeur de l’objet App, juste avant l’appel à InitializeComponent() :

SmartDispatcher.Initialize(Deployment.Current.Dispatcher); 

Le SmartDispatcher est initialisé avec le Dispatcher fourni par le namespace Deployment qui s’occupe majoritairement du manifest et d’informations sur l’application en cours.

La classe de base

Il y a plusieurs façons de l’implémenter, mais puisque j’ai fait le choix de vous montrer l’implémentation de SmartDispatcher, autant utiliser la classe de base qui a été écrite par Wilcox pour fonctionner avec ce dernier, ça semble logique…

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.ComponentModel;
   4:   
   5:  namespace SLMultiThread.ThreadTools
   6:  {
   7:      // (c) Copyright Microsoft Corporation.
   8:      // This source is subject to the Microsoft Public License (Ms-PL).
   9:      // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
  10:      // All other rights reserved.
  11:   
  12:          /// <summary>
  13:          /// A base class for data objects that implement the property changed
  14:          /// interface, offering data binding and change notifications.
  15:          /// </summary>
  16:          public class PropertyChangedBase : INotifyPropertyChanged
  17:          {
  18:              /// <summary>
  19:              /// A static set of argument instances, one per property name.
  20:              /// </summary>
  21:              private static readonly Dictionary<string, PropertyChangedEventArgs> argumentInstances = 
  22:                  new Dictionary<string, PropertyChangedEventArgs>();
  23:   
  24:              /// <summary>
  25:              /// The property changed event.
  26:              /// </summary>
  27:              public event PropertyChangedEventHandler PropertyChanged;
  28:   
  29:              /// <summary>
  30:              /// Notify any listeners that the property value has changed.
  31:              /// </summary>
  32:              /// <param name="propertyName">The property name.</param>
  33:              protected void NotifyPropertyChanged(string propertyName)
  34:              {
  35:                  if (string.IsNullOrEmpty(propertyName))
  36:                  {
  37:                      throw new ArgumentException("PropertyName cannot be empty or null.");
  38:                  }
  39:   
  40:                  var handler = PropertyChanged;
  41:                  if (handler == null) return;
  42:                  PropertyChangedEventArgs args;
  43:                  if (!argumentInstances.TryGetValue(propertyName, out args))
  44:                  {
  45:                      args = new PropertyChangedEventArgs(propertyName);
  46:                      argumentInstances[propertyName] = args;
  47:                  }
  48:   
  49:                  // Fire the change event. The smart dispatcher will directly
  50:                  // invoke the handler if this change happened on the UI thread,
  51:                  // otherwise it is sent to the proper dispatcher.
  52:                  SmartDispatcher.BeginInvoke(() => handler(this, args));
  53:              }
  54:          }
  55:   
  56:  }

Cette classe implémente INotifyPropertyChanged qui est à la base du mécanisme de propagation “automatique” des changements de valeurs utilisé par le binding et sur lequel repose des patterns comme MVVM.

La classe utilise un astuce pour améliorer les performances : elle créée un dictionnaire des PropertyChangedEventArgs de telle façon à ce que ce soit toujours la même instance d’arguments qui soit utilisée pour une propriété donnée. Ainsi, et surtout lorsque les valeurs changent très souvent, le Garbage Collector n’est pas submergé de petits objets à durée de vie ultra-courte. Il est certes optimisé pour cela, mais moins il y a de créations / destructions moins le GC a de travail et tout va plus vite.

Une autre optimisation est importante, elle concerne la détection de la nécessité ou non de transiter via le Dispatcher. Dans le cas où cela n’est pas nécessaire le message de notification suit son cours. Il est ainsi traité immédiatement alors que le passage via le Dispatcher créé un délai (même infime).

Le code exemple

Le projet complet (VS2008 SL3) est téléchargeable ici: SLMultiThread.zip (9,55 kb)

Les autres facettes du multi-thread

Ici nous n’avons fait qu’effleurer le sujet en abordant un problème très spécifique du multithreading sous Silverlight. Si vous regardez le code de plus près, notamment la classe RandomNumberPusher que nous n’avons pas étudiée dans ce billet, vous y trouverez des techniques simples propres au mutithreading comme l’utilisation d’un objet “locker” pour assurer le verrouillage des lectures et écritures entre les threads.

Le sujet est vaste et il serait possible d’en parler des heures. Je réserve cela pour un éventuel futur article, il faut bien en laisser un peu pour plus tard… Une raison de plus de …

Stay Tuned !

Multithreading simplifié

Le multithreading c'est l'épouvantail du développeur. Vous en parlez, hop! tout le monde s'en va de la machine à café... et s'il y en a un qui ne part pas , c'est le genre fanatique qui va débaler une science opaque sur les AppDomains, les mutex et autres mots qui fâchent, du coup, c'est vous qui partez :-)

Je carricature à peine...

C'est tout le problème du multithreading. Pratiqué avec simplicité c'est une technique de plus en plus indispensable pour tirer partie des microprocesseurs multicoeurs et fluidifier les interfaces, mais voilà, comment faire simple avec une telle technique ?

Les puristes vous diront qu'il faut absolument comprendre la technique, et qu'en suite c'est facile... Un peu comme Coluche qui expliquait dans l'un de ses sketchs que son professeur de violon lui avait dit d'apprendre à jouer avec des gants de boxe parce que quand on les enlève ça semble facile...

Je ne vais pas vous dire qu'une démarche rigoureuse est inutile, j'ai un module de multithreading avancé dans mes plans de cours et, bien entendu, voir les choses en profondeur au sein d'une formation est le seul moyen de maîtriser cette technique. Mais il existe aussi des façons simples d'introduire un peu de multitâche dans vos applications.

Il s'agit du composant BackgroundWorker des Windows Forms. Certes le sujet tranche avec mes billets généralement plus orientés vers les super nouveautés hypra fraîches à tel point qu'elles sont même parfois en bêta... Mais il faut bien maintenir les applications existantes, les améliorer, et pour cela il existe, comme le BackgroundWorker des solutions pratiques qui ne nécessitent pas d'installer le framework 3.5 puisque cette classe a été fournie avec .NET 2.0.

De plus, ce composant Windows Forms n'impose pas de connaître les mécanismes du multithreading, il suffit de programmer ces événements comme un bouton. Trop facile ? Peut-être que cela choquera les puristes parce que "cela cache la réalité de ce qui se passe vraiment dans la machine", je leur répondrais que faire du C# au lieu de faire de l'assembleur c'est un peu pareil... Là où je les rejoindrais c'est que, bien entendu, la classe backgroundWorker ne doit pas être utilisée à tord et à travers. Si l'on désire concevoir des classes gérant finement le multitâche, il faut réellement comprendre et donc apprendre. Mais dans de nombreux cas, le BackgroundWorker pourra vous être utile et rendre plus fluide vos applications Windows Forms sans avoir à entrer dans les détails d'une technique un peu aride.

Mais trève de mots, le plus simple c'est de jouer avec ce composant pour se rendre compte de son utilité. Pour facilité la.. tâche... je vous ai préparé un petit projet VS 2005 qui montre comment se servir de backgroundWorker : BGW.zip (41,83 kb)

Un peu de douceur multitâche dans ce monde de multicoeurs...