Dans une optique de fluïdité d'une application client, nous sommes souvent ammené à séparer les traîtements longs de notre thread charger de l'affichage (appellons-le "thread principal). Nous sommes habitué à ce genre de pratique en Windows Form mais qu'en est-il en WPF?
Le dispatcher
Comme vu précédement ( Fonctionnement du Dispatcher (base) ), cet objet "manage" l'acces à l'interface de notre application.
Plus précisément voici à quoi il ressemble :
[code:c#]
using System.Windows.Threading;
class DispatcherObject
{
public Dispatcher Dispatcher {get;}
public bool CheckAccess();
public void VerifyAcess();
}
[/code]
explain check access
explain verifyacces
Règles de bases
Lorsque l'on désire multithreader une application, quelle soit WPF ou non, il y a quelques règles à respecter :
- Ne pas toucher à l'interface si l'on n'est pas dans le thread qui la possède !
- Ne jamais bloquer le thread qui s'occupe de l'affichage (risque de rendre l'utilisateur fou) !
Comment appliquer cette deuxième règles à coup sûr? --> Ne faites jamais rien dans ce thread, lancer toutes vos (longues) opérations en asynchrone !
Par exemple, voici un longue opération :
[code:c#]
public class SlowThing
{
public string Data
{
get
{
Thread.Sleep(10000);
return "hello";
}
}
}
[/code]
Voici ce qu'il ne faut pas faire :
[code:c#]
public void Button1_OnClick(objetc sender, RoutedEventArg e)
{
SlowThing myST = new SlowThing();
Label1.Text = myST.Data;
}
[/code]
Ici, lors du sleep, du "long traîtement" neccessaire à la récupération de notre donnée, l'interface bloquera complettemebt.
Comment résoudre celà ? La méthode la plus simple est sans conteste celle d'utiliser un thread dit worker de notre application.
[code:c#]
public void Button1_OnClick(objetc sender, RoutedEventArg e)
{
ThreadPool.QueueUserWorkItem(
delegate {
SlowThing myST =
new SlowThing();
string result = myST.Data;
Dispatcher.BeginInvoke(DispatcherPriority.Normal, delegate
{
Label1.Text = result;
});
});
}
[/code]
Dans un premier temps, on récupère un Thread du pool de l'application, à qui l'on demande de faire le long traîtement.
Juste après, on demande au Dispatcher qui gère le thread principal de planifier l'oppération d'affectation de la valeur retrouvée à notre interface.
DispatcherPriority
Il existe plusieurs priorités différentes, en voici la liste :
| Nom de la priorité | Explication |
| SystemIdle | Quand le syteme ne fait rien |
| ApplicationIdle | Quand l'application ne fait rien |
| ContextIdle | Quand toutes les background opérations sont finie |
| Background | Quand les opérations prioritaire sont finie |
| Input | Priorité similaire à celle des inputs |
| Loaded | Quand l'affichage s'est terminé et que la priorité va passer la main aux inputs |
| Render | Priorité similaire à l'affichage |
| DataBind | Priorité similaire au DataBinding |
| Normal | Priorité par défaut, avant le DataBinding |
| Send | Priorité absolue, à faire imédiatement |
DispatcherOperation
L'opération Dispatcher.BeginInvoke retourne un objet, un DistacherOperaction.
Ce dernier contient quelques informations et méthodes qui peuvent, dans de très rares cas, être utiles, tel que :
Operations : Wait & Abord
Properties : Priority, Status & Result
Events : Completed & Aborted
Cependant il est fortement déconseillé d'utiliser cet objet.
En effet :
- Wait est stupide en ce qui concerne le thread principal, si le thread principal attend un de ses threads "background", à quoi bon multithreader son application, qui plus est il est très risquer de se retrouver façe à un deadlock...
- Completed peut très bien s'implementer de facon beaucoups plus simple, lancer une opération à la fin de notre opération est une idée à creuser ;)
SynchronizationContext
Le context de synchronisation n'est pas spécific au WPF, en effet, il à fait son apparition dans le .Net 2.0.
Son rôle est approximativement de manager les interaction entre différent thread.
Il possède deux méthode importante : Post & Send
- Post est la méthode qui va nous permettre de lancer une fonction dans un thread de manière synchrone
- Send est la méthode qui va nous permettre de lancer un fonction dans un thread de manière asynchrone
Pour récuperer le SynchrnizationContext, rien de plus simple :
[code:c#]
SynchronizationContext syncContext = SynchronizationContext.Current;
[/code]
Chosing Patern
Dans l'univers du multithread c#, il existe plusieurs méthodes (ou paterns) pour implémenter une situation. Quelles sont ces paterns et lequel choisir pour multithreader une application WPF seront les questions auquelles je vais essayer de répondre ci-dessous.
.Net 1.1 : begin, end, OnCompleted
Il s'agit ici de la plus vielle version, elle consiste en 3 phases :
- BeginAction lance une action asyncrhone
- Quand cette action asynchrone est lancée l'event OnCompleted arrive au thread principal
- EndAction retourne la valeur (l'objet) calculé par l'action asynchrone.
Ce patern est totalement intégré à WPF, ce qui fait que, pour l'utiliser, vous n'aurez rien à faire.
Par exemple, si proxy est un proxy accédant à un web service très lent :
[code:c#]
public void Button1_OnClick(objetc sender, RoutedEventArg e)
{
proxy.BeginGetData(param, OnGetDataComplete, null)
}
private void OnGetDataCompleted(IAsyncResult iar)
{
dara reult = proxy.EndGetData();
resultBox.Text = result;
}
[/code]
Mais, OnGetDataCompleted est toujours exécuté dans le thrad principal, ce qui ne nou aide qu'à moitier.
Pour éviter ce problème, nous pourrons réutiliser Dispatcher.BeginInvoke dans le OnGetDataCompleted....
C'est donc utilisable, mais pas le plus simple.
.Net 2 : Event-based async
Ceci est un bien meilleur choix en ce qui concerne le WPF.
Cependant il n'est pas intégré à WPF pour le moment, mais nous pouvons nous même implementer ce modèle.
Premièrement, comment fonctionne-t-il ?
[code:c#]
public void Button1_OnClick(objetc sender, RoutedEventArg e)
{
WebClient downloader =
new WebClient();
downloader.DownloadDataCompleted +=
new DownloadDataCompletedEventHandler(downloader_DownloadDataCompleted);
downloader.DownloadDataAsync(
new Uri(
"">http://www.simonboigelot.com"));
}
static void downloader_DownloadDataCompleted(object sender, DownloadDataCompletedEventArgs e)
{
byte[] result = e.Result;
}
[/code]
Pourquoi es-ce un meilleur choix, me direz-vous?
Et bien, DownloadDateCompleted est automatiquement exécuté dans un thread différent du thread principal ! (Contrairement au patern précédent)
Malheureusement, ce patern n'est pas disponible de base dans WPF. Comment faire alors? Nous allons écrire une class d'encapsulation au dessu de notre service lent :
[code:c#]
class ServiceWraper
{
SlowService proxi =
new SlowService();
public void GetDataAsync(
object param)
{
SynchronizationContext callerContext = SynchronizationContext.Current;
proxy.BeginGetData(param, ProxyCallBack, callerContext);
}
private void ProxyCallback(IAsynResult iar)
{
int result = proxy.EndGetData(iar);
SynchronizationContext callerContext = iar.AsynState;
callerContext.Post(RaiseEvent, result);
}
private void RaiseEvent(int result)
{
if(OnGetDataCompleted != null)
OnGetDataCompleted(new GetDataCompletedEventArg (result));
}
public event EventHandler<GetDataCompletedEventArg> OnGetDataCompleted;
}
[/code]
Utiliser notre service se fera alors exactement comme utiliser un WebClient :
[code:c#]
public void Button1_OnClick(objetc sender, RoutedEventArg e)
{
ServiceWraper downloader =
new ServiceWraper();
downloader.OnGetDataCompleted+=
new OnGetDataCompletedEventHandler(downloader_OnGetDataCompleted);
downloader.GetDataAsync(param
"http://www.simonboigelot.com"));">);}
static void downloader_OnGetDataCompleted(object sender, OnGetDataCompletedEventArgs e)
{
byte[] result = e.Result;
}
[/code]
Bien sûr il faudra aussi décrire la class GetDataCompletedEventArg.
Bien que ce patern soit plus long à implementer, et peut être un peu lourd à comprendre pour quelqu'un qui débute dans le mutlithreading.Net, une foi mit en place il simplifie vraiment la vie.
Thread Pool patern
Dans ce cas-ci, il n'y a ni bénéfice, ni avantage paticulier. Notez seulement que si l'application dépasse son nombre de thread (entre 40 et 50 dans le pool par défaut) le système peut simplement vous dire : "Je n'ai plus de thread disponible pour cette application, aurevoir + Exception".
Nous l'avons déjà utilisé dans ce document, pour rappel :
[code:c#]
ThreadPool.QueueUserWorkItem(delegate
{
//ajouter la logique du thread (todo)
});
[/code]
Create your own thread patern
Dans ce cas-ci, tout dépend de comment vous allez envisager votre utilisation des thread. Cependant cette remmarque peut être utile :
- plein de thread => attention à trop de thread
Pour eviter de tomber dans ce piège, il est simple d'imaginer un système à 2 thread, un worker en plus du thread principal.
Cette solution est très bienl, on ne bloque pas l'ui, mais on travaille quand meme! Et tout ca sans trops de threads dans tout les sens.
On garde la fluïdité de notre interface tout en restant dans un modèle de programmation simple.
Malheureusement, encore une foi, ce patern n'existe nulle part, vous allez devoir écrire votre propre librairie.
Conclusion
Faire d'une application WPF une application multithreadée demande un peu de réflexion, mais semble indispensable dans l'optique de donner au client une interface qui ne le rendra pas fou. Je vous parlerais bientôt des problèmes lié au DataBinding dans un WPF multithreadé, rester à l'écoute ;)