Salut à tous ! Aujourd’hui, nous allons parler des travailleurs de l’ombre, les threads. Mais pas n’importe quel thread, le background worker, une encapsulation des threads classiques très utile dans le cas d’utilisation d’interface graphique. Ras-le-bol d’avoir un freeze de votre IHM lors d’une connexion à une DB ou un traitement lourd ? Alors le background workers est fait pour vous ^^ (Intervention divine en sus)
Au commencement il n’y avait que le néant et Dieu en haut à gauche, près de la machine à café
Pour commencer à jouer de manière confortable avec les background workers, pas besoin de se retaper la théorie sur les locks, la synchronisation et tout le tsouin-tsouin habituel des threads, 3 events, 2 attributs et 3 méthodes suffisent pour rendre votre application fluide comme un long fleuve tranquille 😀
Pour utiliser les BackgroundWorkers vous devez ajouter une référence à System.Threading. Dans le cadre de ce how to, l’exemple choisi est un chronomètre threadé qui tourne séparément, l’interface étant mise à jour séquentiellement.
Voici le code XAML de l’interface en WPF : http://pastebin.com/TQGDNjJZ
Et voici le code-behind en C# que nous allons compléter au fur et à mesure : http://pastebin.com/QcndwztY
Et le code final qu’il serait bien d’utiliser pour suivre les explications ^^ : http://pastebin.com/2xuy7v2j
Ce qui nous donnera :
Au niveau du code, on a un timespan qui représentera la durée affichée à l’utilisateur et notre bgworker que l’on abordera au point suivant.
Dans l’IHM on a donc un label, qui affichera le temps et trois boutons, start, pour (re)lancer le chronomètre, pause et reset. Le chronomètre pouvant être mis en pause puis reprise ou remis à zéro avec reset.
Une petite précision au passage, il se peut que le chronomètre mette quelques centièmes de seconde à s’arrête ; en fait, le chronomètre s’est bien arrêté au moment du clic, il s’agit juste que la mise à jour du label qui subit un peu de latence. (Elle peut être visible si votre machine rame un peu, ce qui arrive toujours quand on a Outlook, firefox, Word, Visual Studio, le debugger de lancée attaché à l’application et de la musique avec winamp 😉 )
Lors du premier jour Dieu dit « définit les attributs seront » et définis les attributs furent (jeune jedi)
Pour commencer on va initialiser notre objet de manière standard.
Ceci étant fait, il y a deux principaux attributs, ReportProgress et SupportConcelation.
Le premier nous permettra, pendant l’exécution du thread, d’appeler une fonction que l’on définira après. Cette fonction sera appel avec la méthode ReportProgress. Elle est très utile pour mettre à jour pendant le traitement l’interface graphique. Par exemple pour une progress bar ou, dans notre cas, afficher le temps.
En effet, notre chronomètre sera en fait un thread infini qui ne s’arrêtera que quand on le mettra en pause ou quand on le resettera, donc afin de pouvoir mettre à jour l’interface, on utilisera dans cette boucle un appel à cette fonction.
Le second permet d’arrêter à tout moment le thread, sans cela, le thread ne s’arrêtera que lorsque son traitement sera terminé. Ce qui nous arrange pas des masses dans ce cas-ci vu que l’on stoppera le thread à la main ^^:/
On va donc mettre ces deux attributs à true.
Lors du second jour, Dieu dit « codés tes handler seront » et codés les handler furent.
Ensuite on rentre dans le vif du sujet, les backgrounds workers reposent sur les événements. Si vous n’êtes pas familiarisés avec cette notion, reportez-vous à la msdn sur <link MSDN TODO>les routed events</link>. Pour tout de même suivre la suite, les événements peuvent être dans leur forme basique (mais qui suffira ici) des sortes de variables qui prennent comme valeur une méthode. Cette méthode sera exécutée au moment où il sera déclenché au moyen par exemple d’une autre méthode. Par exemple, on peut avoir un événement Work qui une fois déclenché exécute la méthode DisplayNames() et est déclenché par DoWork(), ainsi quand on appelleDoWork, l’événement déclenché exécute la méthode associée DisplayName. L’avantage étant que l’on peut définir de manière dynamique la méthode exécutée par l’événement et, par exemple, l’adapter à la situation.
Ces méthodes, qui sont exécutées lors du déclenchement d’un événement, sont appelées des handlers, que l’on peut traduire par gestionnaires car ils gèrent l’action effectuée lors du déclenchement de l’événement.
Pour les backgrounds workers on a accès à 4 handlers dont 3 seront utilisés ici.
Je vais tout d’abord présenter succinctement les événements et donc leur handler afin d’avoir une vision d’ensemble du processus.
Le premier, le principal, est doWork. C’est le handler qui réalisera le cœur de la tâche que l’on veut threader. Dans notre cas, il incrémentera le chronomètre. Les handlers doivent vous être familiers si vous avez déjà travaillé avec wpf par exemple car très grandement utilisés. Ce thread ne peut dialoguer avec le thread gérant l’ihm, mais les gens de chez Microsoft n’étant pas des branques, ils ont tout prévu et nous verrons plus loin comment contourner de manière élégante ce petit problème.
Ensuite viens le handler reportProgress, ce handler permet de dialoguer directement avec l’ihm. C’est à cet endroit que l’on pourra par exemple mettre à jour l’ihm.
Il existe encore deux événements, RunWorkerCompleted, lui, est exécuté lorsque le bgworker s’est exécuté jusqu’au bout et le dernier, disposed, est exécuté lorsque l’on aura libéré le worker une fois fini.
Le handler pour doWork prend deux arguments, le premier est le bgworker lui-même. L’intérêt de faire cela est que l’on peut ainsi, de l’intérieur du thread obtenir son état ainsi que ses méthodes et événements. Le second argument est un doWorkEventArgs. Il comporte trois champs. Le premier, Arguments, peut être utilisé pour transmettre des arguments au thread lors de son lancement, le second, Cancel, permet de savoir si le thread doit ou non être stoppé et enfin Result permet de stocker la valeur de retour de notre thread qui sera principalement utilisée dans RunWorkerCompleted.
Nous avons maintenant presque tout pour comprendre notre méthode doWork, à la première ligne on récupère le worker passé en argument et on le cast. Ensuite on crée un objet DateTime qui représentera le moment ou le chronomètre est lancé. Ensuite, on entre dans une boucle infinie. Dans cette boucle un if est présent. Il teste la valeur CancellationPending qui sera à true si l’on demande l’arrêt du thread. Si c’est le cas, on sort de la boucle avec un break, sinon on appelle ReportProgress dont voici le contenu :
Report Progress est similaire à DoWork excepté qu’il prend en argument un integer qui représente le pourcentage où en est la tâche et des arguments « libres ». Dans notre cas, on n’utilisera pas le pourcentage, on peut donc le laisser à un à chaque appel. Dans le cas où l’on aurait une progressbar par exemple, cette valeur serait très utile afin de définir l’état d’avancement de la barre. Ensuite on peut passer un second argument de type object, en gros ce qu’on veut.
Le handler prend en argument un ProgressEventChangedArgs qui contient deux arguments, un premier avec la valeur du pourcentage et le second avec l’objet passé en argument. Cette méthode peut interagir avec l’ihm, c’est donc ici que l’on mettra à jour notre label sans que le debugger le hurle à la mort que l’on viole une quelconque restriction de portée 😀
L’objet passé en argument étant de type timespan, on le re-cast, le convertit en string et l’affectons à la valeur du Text du label.
Retournons maintenant sur doWork, pour obtenir le décompte. C’est assez simple, on prend le temps au moment de l’appel de la fonction ProgressChanged moins le temps au moment du début du thread. Et afin de pouvoir relancer le chronomètre, on y ajoute la valeur de last qui est la valeur qu’avait le thread lors de la mise en pause que nous allons voir tout de suite. A chaque itération, on met le thread en pose sinon il va utiliser tout le CPU et bien que l’action soit parallélisée, elle aura tendance à freezer la machine. Ici je mets un sleep de 10 millisecondes ce qui est suffisant pour permettre aux autres process de s’exécuter. (à part quelques chanceux munis de FPGA ou d’un cluster de calcul, on reste quand à quelques process réellement parallèles, 8 étant déjà beaucoup, et quand on sait qu’il y en a des 10énes qui tournent simultanément).
Lorsque l’utilisateur demande à stopper le thread, on sort de la boucle via le break. Ensuite on assigne la valeur courante de, comme pour un progress changed a Result. Ceci permettra de récupérer cette valeur une fois dans la méthode RunWorkerComplete. Notre méthode doWork se termine et c’est maintenant, la dernière, runWorkerComplete qui s’exécute.
Ce qui est intéressant ici c’est le RunWorkerCompletedEventArgs qui contient trois attributs, tout d’abord Cancelled qui permet de savoir si l’opération a été annulée ou non, Error qui récupère les erreurs possibles lors de l’exécution du thread et enfin Result qui contient la valeur que nous avons affectée précédemment dans doWork. Je pourrais très bien ne rien faire ici dans notre cas, mais voulant permettre de mettre en pause le chronomètre, je vais ici stocker dans last la valeur actuelle du chronomètre afin de pouvoir par la suite l’additionner au temps lors de la reprise. Addition que nous effectuons donc dans DoWork au moment du ReportProgress.
Bon, bien y a plus qu’à tester tout cela.
Lors du troisième jour Dieu dit « DoWork tu feras ». Et DoWork il fit.
Pour lancer le bgworker, rien de plus simple :
On vérifie qu’il n’est pas déjà lancé, et si ce n’est pas le cas, on le lance avec ma méthode runWorkerAsync. Et voilà… Il tourne ^^ Au moment où on lance cela, l’événement DoWork est déclenché et donc le handler associé est exécuté.
Ensuite pour le mettre en pause il suffit de vérifier qu’il est bien lancé (il n’appréciera pas que l’on veuille mettre en pause un worker qui ne fait rien) et si c’est le cas on appelle CancelAsync .
Le reste est déjà codé, c’est dans DoWork, via CancellationPending, que l’on sortira de la boucle et vous connaissez la suite.
Enfin, pour le stopper, c’est-à-dire le réinitialiser à zéro, idem que pour la pause sauf que l’on remet ensuite last à nul et, pour une question d’esthétique, le label à « 0 ».
Chacune de ces commandes sont bien entendu bindées dans le code XAML et appelées via un des buttons control. Je prépare en fait un second article, plus ciblé sur WPF, dans lequel je réutiliserai cet exemple de chronomètre, d’où l’utilisation de command et command binding plutôt que le simple mouseclick event handler. Mais ça fonctionnera tout aussi bien avec un event handler, je vous rassure ^^
Lors du quatrième jour, Dieu en glanda pas deux… En utilisant un background worker, il avait gagné la moitié de son temps.
Et voilà, it works ! Pour ceux qui utilisaient les threads classiques, je pense que l’efficacité des bgworkers et surtout leur simplicité d’utilisation n’est plus à démontrer. Avec quelques handlers assez simples on a un thread pleinement fonctionnel. Il est vrai que l’exemple ici est très simple et inutile, mais c’est dans le but de ne pas rajouter à la compréhension des bgworkers, la compréhension de la tâche à exécuter qui n’a pas de rapport avec le sujet. Si j’avais pris un exemple avec une requête sur un serveur de base de données, un traitement lourd et long, un processus serveur/client, une parallélisassion de programme, … la complexité inhérente à la compréhension de la tâche effectuée aurait fortement handicapé ce how to, expliquer le calcul du chronomètre en deux lignes ok, mais tout LinqToX… c’est autre chose…
En résumé, les bgworkers c’est :
3 attributs : Est-ce que je peux le stopper manuellement ? Est-ce qu’il rapporte l’avancement ? Est-ce qu’il attend d’être arrêté ?
4 events : La tâche, le rapport de l’avancement de la tâche, la fin de la tâche, la libération du worker
3 méthodes : RunAsync pour débuter, CancelAsync pour stopper, IsBusy pour savoir son état
Une fois ces quelques éléments en tête, il n’y a plus grand-chose de compliqué.
Lors des 5, 6 et 7émes jours… faut combler… Chabbat bien et vous ?
Le prochain article, qui se basera sur celui-ci, parlera de l’utilisation des notificationt askbar de windows 7 avec WPF4 afin de proposer des contrôles dans la miniature de prévisualisation. Ceci en complément de mon précédent article sur le drag’n’drop.
Et pour répondre aux posts, non je ne vis pas reclus depuis deux semaines, oui je sais, cela fait 2 semaines que je suis à presque 2 articles par jour et surtout non ce n’est pas du copié/collé, d’ailleurs, j’ai vu il y a 3 jours que c’était l’inverse (hein Rodt-G :p )
Inform@tiquement
Istace Emmanuel – Manu404