63
Partie 6 – Création de solutions professionnelles © Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp Partie VI Création de solutions professionnelles Dans cette partie : Introduction à la Bibliothèque parallèle de tâches Accès parallèle aux données Création et utilisation d’un service Web

Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Embed Size (px)

Citation preview

Page 1: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Partie VI

Création de solutions professionnelles

Dans cette partie : Introduction à la Bibliothèque parallèle de tâches Accès parallèle aux données Création et utilisation d’un service Web

Page 2: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

Chapitre 27

Introduction à la Bibliothèque parallèle de tâches (TPL)

Au terme de ce chapitre, vous saurez :

■ Décrire les bénéfices de l’implémentation d’opérations parallèles dans une application.

■ Expliquer comment la Bibliothèque parallèle de tâches fournit une plateforme optimale pour implémenter des applications qui tirent profit d’un processeur multicœurs.

■ Utiliser la classe Task pour créer et exécuter des opérations parallèles dans une application.

■ Utiliser la classe Parallel pour paralléliser certaines constructions de programmation courantes.

■ Utiliser des tâches avec des threads pour améliorer la réactivité et le débit dans des applications graphiques.

■ Annuler des tâches de longue durée et gérer des exceptions levées par des opérations parallèles.

Vous avez appris à utiliser Microsoft Visual C# pour construire des applications qui fournissent une interface utilisateur graphique et qui peuvent gérer les informations stockées dans une base de données. Ces fonctionnalités sont courantes dans la plupart des systèmes modernes. Cependant, les exigences des utilisateurs ont évolué au même rythme que la technologie, si bien que les applications qui permettent d’accomplir les tâches quotidiennes doivent fournir des solutions toujours plus sophistiquées. Dans la dernière partie de ce livre, vous verrez quelques-unes des fonctionnalités avancées introduites dans le .NET Framework 4.0. En particulier, dans ce chapitre, vous verrez comment améliorer les traitements simultanés dans une application grâce à la Bibliothèque parallèle de tâches. Au chapitre suivant, vous verrez comment les extensions parallèles fournies avec le .NET Framework peuvent être utilisées en conjonction avec LINQ (Language

Page 3: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Integrated Query) pour améliorer le débit des opérations d’accès aux données. Dans le dernier chapitre, vous aborderez Windows Communication Foundation pour construire des solutions distribuées qui peuvent incorporer des services s’exécutant sur plusieurs ordinateurs. En guise de bonus, l’annexe décrit comment utiliser le Dynamic Language Runtime pour construire des applications C# et des composants qui peuvent interopérer avec des services créés avec d’autres langages opérant en dehors de la structure fournie par le .NET Framework, tels que Python et Ruby.

Dans les précédents chapitres de ce livre, vous avez appris à utiliser C# pour écrire des programmes qui s’exécutent dans un seul thread, ce qui signifie qu’à un moment donné, un programme n’exécute qu’une seule instruction. Cette approche n’est pas toujours la plus efficace. Par exemple, vous avez vu dans le chapitre 23, « Saisie utilisateur », que si votre programme attend que l’utilisateur clique sur un bouton dans un formulaire WPF (Windows Presentation Foundation), il peut accomplir une autre tâche pendant ce temps-là. Cependant, si un programme mono-thread doit effectuer un long calcul sollicitant beaucoup le processeur, il ne peut pas réagir quand l’utilisateur saisit des données dans un formulaire ou clique sur un menu. L’utilisateur a alors l’impression que l’application a planté. Ce n’est que lorsque le calcul est terminé que l’interface utilisateur réagit à nouveau. Les applications qui peuvent effectuer plusieurs tâches en même temps optimisent les ressources disponibles sur un ordinateur, s’exécutent plus rapidement et sont plus réactives. De plus, certaines tâches individuelles peuvent s’exécuter bien plus rapidement si on les divise en chemins d’exécution parallèles tournant de façon simultanée. Dans le chapitre 23, vous avez vu comment WPF peut tirer profit des threads pour améliorer la réactivité d’une interface utilisateur graphique. Dans ce chapitre, vous allez apprendre à utiliser la Bibliothèque parallèle de tâches pour implémenter une forme plus générique de traitement multitâche dans vos programmes qui peut s’appliquer aux applications de calcul intensif et pas seulement à celles gérant des interfaces utilisateur.

Pourquoi gérer le mode multitâche par un traitement parallèle ?

Comme nous l’avons déjà évoqué dans l’introduction, il y a deux raisons

Page 4: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

principales qui motivent le choix du mode multitâche dans une application :

■ Amélioration de la réactivité Vous pouvez donner à l’utilisateur l’impression que le programme effectue plus d’une tâche à la fois en divisant le programme en threads d’exécution concurrents et en permettant à chaque thread de s’exécuter à tour de rôle pendant une courte période de temps. Il s’agit du modèle conventionnel coopératif que connaissent bien les développeurs Windows chevronnés. Cependant, ce n’est pas véritablement du mode multitâche car le processeur est partagé entre les threads, et la nature coopérative de cette approche nécessite que le code exécuté par chaque thread se comporte de manière appropriée. Si un thread accapare le CPU et les ressources disponibles au détriment des autres threads, cette approche n’a plus aucun intérêt. Il est parfois difficile d’écrire des applications performantes qui suivent ce modèle de façon cohérente.

■ Amélioration de l’évolutivité Vous pouvez améliorer l’évolutivité grâce à une gestion efficace des ressources de traitement disponibles et en utilisant ces ressources pour réduire le temps nécessaire à l’exécution des parties d’une application. Un développeur peut déterminer quelles parties d’une application peuvent tourner en parallèle et faire en sorte qu’elles s’exécutent simultanément. Plus les ressources informatiques augmentent, plus les tâches peuvent s’exécuter en parallèle. Jusqu’à un passé récent, ce modèle ne convenait qu’aux systèmes qui possédaient plusieurs CPU ou étaient capables de répartir le traitement sur plusieurs ordinateurs en réseau. Dans les deux cas, on devait utiliser un modèle capable de coordonner les tâches parallèles. Microsoft fournit une version spécialisée de Windows appelée High Performance Compute (HPC) Server 2008, qui permet à une entreprise de créer des clusters de serveurs pouvant distribuer et exécuter des tâches en parallèle. Les développeurs peuvent utiliser l’implémentation Microsoft du MPI (Message Passing Interface), un protocole de communication indépendant des langages réputé, pour concevoir des applications basées sur des tâches parallèles qui se coordonnent et coopèrent les unes avec les autres en s’envoyant des messages. Les solutions basées sur Windows HPC Server 2008 et le MPI sont idéales pour des applications scientifiques et industrielles de grande envergure, mais elles sont coûteuses pour des micro-ordinateurs sous dimensionnés.

Dans ces conditions, vous pouvez penser que la manière la plus rentable de concevoir des solutions multitâches pour des applications bureautiques est

Page 5: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

l’approche coopérative multithread. Cependant, cette approche n’était conçue que pour offrir de la réactivité (les ordinateurs avec un seul processeur pouvaient garantir que chaque tâche possédait une part équitable du processeur). Cela ne convient pas pour les machines multiprocesseurs car ce n’est pas conçu pour répartir la charge entre les processeurs et, par conséquent, cela est peu évolutif. Quand les machines de bureau multiprocesseurs coûtaient cher (elles étaient donc relativement rares), ce n’était pas un problème. Cependant, la situation a changé, comme je vais l’expliquer brièvement.

Apparition des processeurs multicœurs Il y a dix ans, le coût d’un ordinateur personnel standard se situait dans une fourchette allant de 500 à 1000 euros. Aujourd’hui, un ordinateur personnel correct coûte toujours à peu près le même prix, même après dix ans d’inflation. Un PC typique de nos jours comprend un processeur s’exécutant à une vitesse de 2 à 3 GHz, un disque dur de 500 Go, 4 Go de RAM, une carte vidéo haute résolution, et un graveur DVD. Il y a dix ans, les caractéristiques d’une machine standard étaient un processeur s’exécutant à une vitesse de 500 MHz à 1 GHz, un disque dur de 80 Go et Windows s’exécutait assez bien avec 256 Mo de RAM, les graveurs de CD coûtant plus de 100 euros (les graveurs de DVD étaient rares et extrêmement chers). Ce sont les joies du progrès technologique : du matériel plus rapide et plus puissant à des prix de plus en plus bas.

Ceci n’est pas une nouvelle tendance. En 1965, Gordon E. Moore, co-fondateur d’Intel, écrivit un article intitulé « Cramming more components onto integrated circuits », qui expliquait pourquoi la miniaturisation croissante des composants permettait à un plus grand nombre de transistors d’être intégrés sur une puce. En 1975, la baisse des coûts de production combinée à une technologie plus accessible permettait l’assemblage de 65 000 composants sur une seule puce. Les observations de Moore ont engendré ce que l’on appelle la « loi de Moore » qui stipule que le nombre de transistors d’un circuit intégré augmente de façon exponentielle, doublant approximativement tous les deux ans (en fait, Gordon Moore était au départ encore plus optimiste que cela, car il postulait que le volume des transistors doublerait tous les ans, mais il modifia ultérieurement ses calculs). La possibilité d’agréger des transistors engendra une transmission des données plus rapide entre les composants. Cela signifiait qu’on pouvait s’attendre à ce que les fondeurs de puces produisent des microprocesseurs plus rapides et

Page 6: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

plus puissants à un rythme presque sans relâche, ce qui permettrait aux développeurs d’applications d’écrire des logiciels encore plus compliqués qui s’exécuteraient plus rapidement.

La loi de Moore concernant la miniaturisation des composants électroniques est toujours valable, même après plus de 40 ans. Cependant, la physique a commencé à intervenir. Il y a une limite où il n’est pas possible de transmettre plus rapidement des signaux entre les transistors sur une seule puce, quelle que soit la finesse de gravure ou la densité. Pour un développeur de logiciel, le résultat le plus notable de cette limite est que les processeurs ont cessé d’être plus rapides. Il y a six ans, un processeur rapide tournait à 3 GHz. Aujourd’hui, un processeur rapide s’exécute toujours à 3 GHz.

La limite de la vitesse à laquelle les processeurs peuvent transmettre des données entre les composants a amené les fondeurs de puces à chercher d’autres voies pour accroître la quantité de travail qu’un processeur peut réaliser. Le résultat est que les processeurs les plus modernes ont désormais deux cœurs voire plus. Les fabricants de puces ont mis plusieurs processeurs sur la même puce et ont ajouté la logique nécessaire pour leur permettre de communiquer et de se coordonner les uns avec les autres. Les processeurs dual-core (deux cœurs) et les processeurs quad-core (quatre cœurs) sont devenus courants aujourd’hui. Les puces avec 8, 16, 32, et 64 cœurs sont disponibles, et on s’attend à ce que leur prix chute dans un futur proche. Par conséquent, bien que les processeurs aient cessé d’augmenter leur vitesse, on peut s’attendre à en avoir plus sur une même puce.

Qu’est-ce que cela signifie pour un développeur d’applications C# ?

Avant les processeurs multicœurs, une application mono-thread ne pouvait être accélérée qu’en s’exécutant sur un processeur plus rapide. Avec les processeurs multicœurs, ce n’est plus le cas. Une application mono-thread s’exécutera à la même vitesse sur un processeur single-core, dual-core, ou quad-core ayant la même fréquence d’horloge. La différence est que sur un processeur dual-core, un des cœurs du processeur sera presque inactif, et sur un processeur quad-core, trois des cœurs attendront du travail. Pour utiliser au mieux les processeurs multicœurs, vous devez écrire vos applications en tirant parti du mode multitâche.

Page 7: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Implémentation du mode multitâche dans une application bureautique

Le mode multitâche est la faculté de faire plusieurs choses en même temps. C’est un de ces concepts qui est facile à décrire mais qui, jusqu’à un passé récent, était difficile à implémenter.

Dans un scénario optimal, une application s’exécutant sur un processeur multicœurs effectue autant de tâches concurrentes qu’il y a de cœurs de processeur disponibles, en occupant chacun des cœurs. Cependant, il y a de nombreux problèmes à prendre en considération pour implémenter la concurrence, notamment les questions suivantes :

■ Comment peut-on diviser une application en un ensemble d’opérations concurrentes ?

■ Comment peut-on s’organiser pour qu’un ensemble d’opérations s’exécute en même temps, sur plusieurs processeurs ?

■ Comment peut-on assurer qu’on effectue autant d’opérations concurrentes qu’on a de processeurs disponibles ?

■ Si une opération est bloquée (elle attend par exemple la fin d’une opération d’entrée/sortie), comment peut-on détecter cela et s’organiser pour que le processeur exécute une opération différente au lieu d’être inactif ?

■ Comment peut-on déterminer qu’une ou plusieurs opérations concurrentes sont terminées ?

■ Comment peut-on synchroniser l’accès aux données partagées pour garantir que plusieurs opérations concurrentes ne corrompent pas par inadvertance leurs données ?

Pour un développeur d’applications, le premier problème est une question de conception d’application. Les problèmes restants dépendent de l’infrastructure de programmation (Microsoft fournit la Bibliothèque parallèle de tâches pour vous aider à résoudre ces problèmes).

Dans le chapitre 28, « Accès parallèle aux données », vous allez voir comment certains problèmes de requêtes possèdent naturellement des solutions parallèles,

Page 8: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

et comment on peut utiliser le type ParallelEnumerable de PLINQ pour paralléliser des opérations de requêtes. Cependant, on a parfois besoin d’une approche plus essentielle pour des situations plus généralistes. La Bibliothèque parallèle de tâches (TPL) contient une série de types et d’opérations qui vous permettent de spécifier plus explicitement comment diviser une application en un ensemble de tâches parallèles.

Tâches, threads et ThreadPool Le type le plus important dans la TPL est la classe Task. La classe Task est une abstraction d’une opération concurrente. On crée un objet Task pour exécuter un bloc de code. On peut instancier plusieurs objets Task et démarrer leur exécution en parallèle s’il y a suffisamment de processeurs ou de cœurs de processeur disponibles.

Note À partir de maintenant, j’utiliserai le terme « processeur » pour faire référence soit à un processeur single-core soit à un seul cœur de processeur sur un processeur multicœurs.

En interne, la TPL implémente les tâches et planifie leur exécution en utilisant des objets Thread et la classe ThreadPool. Le multithreading et les pools de threads sont disponibles dans le .NET Framework depuis la version 1.0, et vous pouvez utiliser la classe Thread de l’espace de noms System.Threading directement dans votre code. Cependant, la TPL fournit un degré supérieur d’abstraction qui permet facilement de faire la distinction entre le degré de parallélisation d’une application (les tâches) et les unités de parallélisation (les threads). Sur un ordinateur avec un seul processeur, ces éléments sont habituellement les mêmes. Cependant, sur un ordinateur avec plusieurs processeurs ou avec un processeur multicœurs, ils sont différents. Si vous concevez un programme basé directement sur les threads, vous allez trouver que votre application ne peut pas évoluer facilement ; le programme utilisera le nombre de threads que vous créez explicitement, et le système d’exploitation planifiera uniquement ce nombre de threads. Cela peut conduire à une surcharge et à des temps de réponse dégradés si le nombre de threads excède de beaucoup le nombre de processeurs disponibles, ou à une gestion inefficace et un faible débit si le nombre de threads est inférieur au nombre de processeurs.

La TPL optimise le nombre de threads nécessaire pour implémenter un ensemble

Page 9: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

de tâches concurrentes et les planifie de manière efficace en fonction du nombre de processeurs disponible. La TPL utilise un ensemble de threads fournis par le .NET Framework, appelé ThreadPool, et implémente un mécanisme de file d’attente pour distribuer la charge de travail entre ces threads. Quand un programme crée un objet Task, la tâche est ajoutée à une file d’attente globale. Quand un thread devient disponible, la tâche est supprimée de la file d’attente globale, puis elle est exécutée par ce thread. ThreadPool implémente un certain nombre de mécanismes d’optimisations et utilise un algorithme de vol de travail (work-stealing) pour s’assurer que ces threads sont planifiés de manière efficace.

Note ThreadPool était disponible dans les éditions précédentes du .NET Framework, mais il .NET Framework 4.0 afin de prendre en charge les objets Tasks.

Vous devez noter que le nombre de threads créés par le .NET Framework pour prendre en charge vos tâches est n’est pas nécessairement identique au nombre de processeurs. En fonction de la nature de la charge de travail, un ou plusieurs processeurs peuvent être occupés à accomplir un travail prioritaire pour d’autres applications et services. En conséquence, le nombre optimal de threads de votre application peut être inférieur au nombre de processeurs de l’ordinateur. D’un autre côté, un ou plusieurs threads d’une application peuvent attendre la fin d’une opération particulièrement longue (accès mémoire, entrée/sortie, réseau, etc.), ce qui rend disponibles les processeurs correspondants. Dans ce cas, le nombre optimal de threads peut être supérieur au nombre de processeurs disponibles. Le .NET Framework adopte une stratégie itérative, appelée algorithme d’escalade de colline (hill-climbing), pour déterminer de manière dynamique le nombre idéal de threads de la charge de travail en cours.

La chose importante à retenir est que vous devez vous contenter dans votre code de diviser votre application en tâches qui peuvent être exécutées en parallèle. Le .NET Framework se charge de créer le nombre de threads approprié en se basant sur l’architecture des processeurs et la charge de travail de l’ordinateur, puis associe vos tâches à ces threads en les organisant pour qu’ils soient exécutés efficacement. Ce n’est pas grave si vous divisez votre travail en trop de tâches car le .NET Framework tentera de n’exécuter que le nombre maximal de threads concurrents réellement possibles ; en fait, vous êtes encouragé à surpartitionner votre travail car cela garantit que votre application pourra évoluer si vous la

Page 10: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

déplacez sur un ordinateur qui a plus de processeurs disponibles.

Création, exécution et contrôle des tâches L’objet Task et les autres types de la TPL résident dans l’espace de noms System.Threading.Tasks. On crée des objets Task avec le constructeur Task. Ce constructeur est surchargé, mais toutes les versions attendent la fourniture d’un délégué Action comme paramètre. Au chapitre 23, vous avez vu qu’un délégué Action référence une méthode qui ne retourne pas de valeur. Un objet Task utilise ce délégué pour exécuter la méthode quand il est planifié. L’exemple suivant crée un objet Task qui utilise un délégué pour exécuter la méthode appelée faireTravail (vous pouvez aussi utiliser une méthode anonyme ou une expression lambda, comme cela est suggéré dans les commentaires du code) :

Task = new Task(new Action(faireTravail)); // Task = new Task(delegate { this.faireTravail(); }); // Task task = new Taskhis.faireTravail(); }); ... private void faireTravail() { // La tâche exécute ce code quand elle est démarrée ... }

Note Dans de nombreux cas, vous pouvez laisser le compilateur déduire le type de délégué Action et spécifier simplement la méthode à exécuter. Par exemple, vous pouvez réécrire le premier exemple de la manière suivante :

Task task = new Task(faireTravail);

Les règles d’inférence des délégués implémentées par le compilateur ne s’appliquent pas qu’au type Action, mais à n’importe quel endroit où l’on peut utiliser un délégué. Nous verrons d’autres exemples de ce type dans le reste de cet ouvrage.

Le type Action par défaut référence une méthode qui n’accepte pas de paramètre. Les autres surchargements du constructeur Task prennent un paramètre Action<objet> qui représente un délégué qui fait référence à une méthode acceptant un seul paramètre objet. Ces surchargements permettent de passer des données à la méthode exécutée par la tâche. Le code suivant illustre un exemple :

Action<objet> action;

Page 11: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

action = doWorkWithObject; objet parameterData = ...; Task task = new Task(action, parameterData); ... private void doWorkWithObject(objet o) { ... }

Après avoir créé un objet Task, vous pouvez l’exécuter avec la méthode Start :

Task task = new Task(...); task.Start();

La méthode Start est aussi surchargée, et vous pouvez spécifier en option un objet TaskScheduler pour contrôler le degré de concurrence et les autres options d’ordonnancement. Il est recommandé d’utiliser l’objet TaskScheduler par défaut qui est intégré dans le .NET Framework ; vous pouvez aussi définir votre propre classe TaskScheduler si vous voulez exercer un contrôle plus strict sur la manière dont les tâches sont mises en file d’attente et planifiées. Les détails de ces opérations dépassent le cadre de cet ouvrage, mais vous trouverez de plus amples informations sur ce sujet dans la description de la classe abstraite TaskScheduler qui est disponible dans la documentation de la bibliothèque de classes du .NET Framework fournie avec Visual Studio.

Vous pouvez obtenir une référence à l’objet TaskScheduler par défaut grâce à la propriété statique Default de la classe TaskScheduler. La classe TaskScheduler fournit aussi la propriété statique Current, qui retourne une référence à l’objet TaskScheduler en cours d’utilisation (cet objet TaskScheduler est utilisé si vous ne spécifiez pas explicitement un planificateur). Une tâche peut fournir des informations à l’objet TaskScheduler par défaut sur la manière de planifier et d’exécuter la tâche si vous spécifiez une valeur à partir de l’énumération TaskCreationOptions du constructeur Task. Pour de plus amples informations sur l’énumération TaskCreationOptions, consultez la documentation de la bibliothèque de classes du .NET Framework fournie avec Visual Studio.

Quand la méthode exécutée par la tâche est achevée, la tâche se termine, et le thread utilisé pour exécuter la tâche peut être recyclé pour exécuter une autre tâche.

Normalement, le planificateur s’organise pour accomplir les tâches en parallèle

Page 12: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

quand cela est possible, mais vous pouvez aussi planifier les tâches en série en créant une continuation. On réalise une continuation en appelant la méthode ContinueWith d’un objet Task. Quand l’action accomplie par l’objet Task se termine, le planificateur crée automatiquement un nouvel objet Task pour exécuter l’action spécifiée par la méthode ContinueWith. La méthode spécifiée par la continuation attend un paramètre Task, et le planificateur passe une référence à la tâche qui a achevé la méthode. La valeur retournée par ContinueWith est une référence au nouvel objet Task. L’exemple de code suivant crée un objet Task qui exécute la méthode faireTravail et spécifie une continuation qui exécute la méthode faireAutreTravail dans une nouvelle tâche quand la première tâche se termine :

Task task = new Task(faireTravail); task.Start(); Task newTask = task.ContinueWith(faireAutreTravail); ... private void faireTravail() { // La tâche exécute ce code quand elle est démarrée ... } ... private void faireAutreTravail(Task task) { // La continuation exécute ce code quand faireTravail se termine ... }

La méthode ContinueWith est lourdement surchargée, et vous pouvez fournir un grand nombre de paramètres qui spécifient des éléments supplémentaires, comme TaskScheduler pour utiliser une valeur TaskContinuationOptions. Le type TaskContinuationOptions est une énumération qui contient un surensemble des valeurs de l’énumération TaskCreationOptions. Parmi les valeurs supplémentaires, on peut citer :

■ NotOnCanceled et OnlyOnCanceled L’option NotOnCanceled spécifie que la continuation ne doit s’exécuter que si l’action précédente est terminée et n’est pas annulée, et l’option OnlyOnCanceled spécifie que la continuation ne doit s’exécuter que si l’action précédente est annulée. La section « Annulation des tâches et gestion des exceptions » plus loin dans ce chapitre décrit comment annuler une tâche.

■ NotOnFaulted et OnlyOnFaulted L’option NotOnFaulted indique que la

Page 13: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

continuation ne doit s’exécuter que si l’action précédente est terminée et ne lève pas une exception non gérée. L’option OnlyOnFaulted provoque l’exécution de la continuation seulement si l’action précédente lève une exception non gérée. La section « Annulation des tâches et gestion des exceptions » fournit plus d’informations sur la manière de gérer des exceptions dans une tâche.

■ NotOnRanToCompletion et OnlyOnRanToCompletion L’option NotOnRanToCompletion spécifie que la continuation ne doit s’exécuter que si la précédente action ne se termine pas avec succès ; elle doit être annulée ou bien lever une exception. OnlyOnRanToCompletion provoque l’exécution de la continuation seulement si l’action précédente se termine avec succès.

L’exemple de code suivant montre comment ajouter une continuation à une tâche qui ne s’exécute que si l’action initiale ne lève pas une exception non gérée :

Task task = new Task(faireTravail); task.ContinueWith(faireAutreTravail, TaskContinuationOptions.NotOnFaulted); task.Start();

Si vous utilisez habituellement le même ensemble de valeurs TaskCreationOptions et le même objet TaskScheduler, vous pouvez utiliser un objet TaskFactory pour créer et exécuter une tâche en une seule étape. Le constructeur de la classe TaskFactory permet de spécifier le planificateur de tâches, les options de création de tâches, et les options de continuation de tâches que les tâches construites par cette fabrique doivent utiliser. La classe TaskFactory fournit la méthode StartNew pour créer et exécuter un objet Task. Comme la méthode Start de la classe Task, la méthode StartNew est surchargée, mais toutes les deux attendent une référence à une méthode que la tâche doit exécuter.

Le code suivant illustre un exemple qui crée et exécute deux tâches utilisant la même fabrique de tâches :

TaskScheduler scheduler = TaskScheduler.Current; TaskFactory taskFactory = new TaskFactory(scheduler, TaskCreationOptions.None, TaskContinuationOptions.NotOnFaulted); Task task = taskFactory.StartNew(faireTravail); Task task2 = taskFactory.StartNew(faireAutreTravail);

Même si vous ne spécifiez pas actuellement des options de création de tâches particulières et utilisez le planificateur de tâches par défaut, vous devez quand

Page 14: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

même envisager d’utiliser un objet TaskFactory ; cela garantit la cohérence, et vous aurez moins de code à modifier pour vous assurer que toutes les tâches s’exécutent de la même manière si vous avez besoin de personnaliser le processus à l’avenir. La classe Task expose l’objet TaskFactory par défaut utilisé par la TPL via la propriété statique Factory. Vous pouvez l’utiliser de la manière suivante :

Task task = Task.Factory.StartNew(faireTravail);

La synchronisation des tâches est un besoin courant des applications qui invoquent des opérations en parallèle. La classe Task fournit la méthode Wait qui implémente une simple méthode de coordination de tâches. Elle permet de suspendre l’exécution du thread en cours jusqu’à ce que la tâche spécifiée se termine :

task2.Wait(); // Attend que task2 se termine

Vous pouvez attendre un ensemble de tâches en utilisant les méthodes statiques WaitAll et WaitAny de la classe Task. Ces deux méthodes prennent un tableau params contenant un ensemble d’objets Task. La méthode WaitAll attend jusqu’à ce que toutes les tâches spécifiées soient terminées, et WaitAny attend jusqu’à ce qu’au moins une des tâches spécifiées se termine. On utilise ces méthodes de la manière suivante :

Task.WaitAll(task, task2); // Attend que task et task2 soient terminées Task.WaitAny(task, task2); // Attend que task ou task2 soit terminée

Utilisation de la classe Task pour implémenter le parallélisme Dans l’exercice suivant, vous allez utiliser la classe Task pour paralléliser le code d’une application qui sollicite beaucoup le processeur, et vous allez voir comment cette parallélisation réduit le temps d’exécution de l’application en répartissant les calculs sur plusieurs cœurs de processeurs.

L’application, appelée GraphDemo, comprend un formulaire WPF qui utilise un contrôle Image pour afficher un graphique. L’application dessine les points du graphique en accomplissant un calcul complexe.

Page 15: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Note Les exercices de ce chapitre sont conçus pour s’exécuter sur un ordinateur doté d’un processeur multicœurs. Si vous ne possédez qu’un CPU single-core, vous n’observez pas les mêmes effets. Vous ne devez pas non plus démarrer d’autres programmes ou services entre les exercices car cela peut affecter les résultats constatés.

Observation et exécution de l’application mono-thread GraphDemo

1. Démarrez Microsoft Visual Studio 2010 s’il n’est pas déjà ouvert.

2. Ouvrez la solution GraphDemo, situé dans le dossier \Visual C Sharp Etape par étape\Chapitre 27\GraphDemo de votre dossier Documents.

3. Dans l’Explorateur de solutions, dans le projet GraphDemo, faites un double clic sur le fichier GraphWindow.xaml pour afficher le formulaire dans la fenêtre Design.

Le formulaire contient les contrôles suivants :

■ Un contrôle Image appelé graphImage. Ce contrôle image affiche le graphique affiché par l’application.

■ Un contrôle Button appelé tracerButton. L’utilisateur clique sur ce bouton pour générer les données du graphique et les afficher dans le contrôle graphImage.

■ Un contrôle Label appelé duree. L’application affiche le temps mis pour générer et afficher les données du graphique dans ce label.

4. Dans l’Explorateur de solutions, développez GraphWindow.xaml, puis faites un double clic sur GraphWindow.xaml.cs pour afficher le code du formulaire dans la fenêtre Code.

Le formulaire utilise un objet System.Windows.Media.Imaging.WriteableBitmap appelé graphBitmap pour afficher le graphique. Les variables largeurPixel et hauteurPixel spécifient respectivement la résolution horizontale et verticale de l’objet WriteableBitmap ; les variables dpiX et dpiY spécifient respectivement la densité horizontale et verticale de l’image en points par pouce (dpi pour dots per inch) :

public partial class GraphWindow : Window

Page 16: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

{ private static long tailleMemoireDisponible = 0; private int largeurPixel = 0; private int hauteurPixel = 0; private double dpiX = 96.0; private double dpiY = 96.0; private WriteableBitmap graphBitmap = null; … }

5. Examinez le constructeur GraphWindow. Il ressemble à ceci :

public GraphWindow() { InitializeComponent(); PerformanceCounter compteurMem = new PerformanceCounter("Mémoire", "Octets disponibles"); tailleMemoireDisponible = Convert.ToUInt64(compteurMem.NextValue()); this.largeurPixel = (int)tailleMemoireDisponible / 20000; if (this.largeurPixel < 0 || this.largeurPixel > 15000) this.largeurPixel = 15000; this.hauteurPixel = (int)tailleMemoireDisponible / 40000; if (this.hauteurPixel < 0 || this.hauteurPixel > 7500) this.hauteurPixel = 7500; }

Afin d’éviter d’avoir du code qui épuise la mémoire disponible de votre ordinateur et génère des exceptions OutOfMemory, cette application crée un objet PerformanceCounter qui interroge la quantité de mémoire physique disponible de l’ordinateur. On utilise alors cette information pour déterminer les valeurs appropriées des variables largeurPixel et hauteurPixel. Plus vous avez de mémoire disponible sur votre ordinateur, plus les valeurs générées pour largeurPixel et hauteurPixel sont grandes (dans les limites respectives de 15 000 et 7 500 pour chacune de ces variables) et plus vous verrez les bénéfices de l’emploi de la TPL au fur et à mesure des exercices de ce chapitre. Cependant, si vous trouvez que l’application génère toujours des exceptions OutOfMemory, augmentez les diviseurs (20 000 et 40 000) utilisés pour générer les valeurs de largeurPixel et hauteurPixel.

Si vous avez beaucoup de mémoire, les valeurs calculées pour largeurPixel et hauteurPixel peuvent déborder. Dans ce cas, elles contiendront des valeurs négatives et l’application échouera en générant plus tard une exception. Le code du constructeur vérifie ce cas et initialise les champs largeurPixel et

Page 17: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

hauteurPixel à une paire de valeurs utiles pour permettre à l’application de s’exécuter correctement dans cette situation.

6. Examinez le code de la méthode tracerButton_Click :

private void tracerButton_Click(objet sender, RoutedEventArgs e) { if (graphBitmap == null) { graphBitmap = new WriteableBitmap(largeurPixel, hauteurPixel, dpiX, dpiY, PixelFormats.Gray8, null); } int octetsParPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8; int stride = octetsParPixel * graphBitmap.PixelWidth; int tailleDonnees = stride * graphBitmap.PixelHeight; byte [] donnees = new byte[tailleDonnees]; Stopwatch montre = Stopwatch.StartNew(); genererDonneesGraph(donnees); duree.Content = string.Format("Durée (ms) : {0}", montre.ElapsedMilliseconds); graphBitmap.WritePixels( new Int32Rect(0, 0, graphBitmap.PixelWidth, graphBitmap.PixelHeight), donnees, stride, 0); graphImage.Source = graphBitmap; }

Cette méthode s’exécute lorsque l’utilisateur clique sur le bouton tracerButton. Le code instancie l’objet graphBitmap s’il n’a pas déjà été créé par l’utilisateur en cliquant sur le bouton tracerButton précédemment, et il spécifie que chaque pixel représente une ombre grise, avec 8 bits par pixel. Cette méthode utilise les variables et méthodes suivantes :

■ La variable octetsParPixel calcule le nombre d’octets requis pour contenir chaque pixel (le type WriteableBitmap prend en charge une grande plage de formats de pixels, avec des pixels allant jusqu’à 128 bits pour les images en millions de couleurs).

■ La variable stride contient la distance verticale, en octets, entre les pixels adjacents de l’objet WriteableBitmap.

■ La variable tailleDonnees calcule le nombre d’octets requis pour contenir les données de l’objet WriteableBitmap. Cette variable est utilisée pour initialiser le tableau donnees avec la taille appropriée.

Page 18: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

■ Le tableau byte donnees contient les données du graphique.

■ La variable montre est un objet System.Diagnostics.Stopwatch. Le type StopWatch est utile pour les opérations de chronométrage. La méthode statique StartNew du type StopWatch crée une nouvelle instance d’un objet StopWatch et démarre son exécution. Vous pouvez interroger le temps d’exécution d’un objet StopWatch en examinant la propriété ElapsedMilliseconds.

■ La méthode genererDonneesGraph remplit le tableau donnees avec les données du graphique pour qu’elles soient affichées par l’objet WriteableBitmap. Vous allez examiner cette méthode dans la prochaine étape.

■ La méthode WritePixels de la classe WriteableBitmap copie les données d’un tableau byte dans un bitmap à afficher. Cette méthode prend un paramètre Int32Rect qui spécifie la zone de l’objet WriteableBitmap à remplir, les données à utiliser pour copier vers l’objet WriteableBitmap, la distance verticale entre les pixels adjacents de l’objet WriteableBitmap, et un décalage vers l’objet WriteableBitmap pour commencer à y écrire les données.

Note Vous pouvez utiliser la méthode WritePixels pour écraser de manière sélective les informations d’un objet WriteableBitmap. Dans cet exemple, le code écrase tout le contenu. Pour plus d’informations sur la classe WriteableBitmap, consultez la documentation de la bibliothèque de classes du .NET Framework installée avec Visual Studio 2010.

■ La propriété Source d’un contrôle Image spécifie les données que le contrôle Image doit afficher. Cet exemple définit la propriété Source avec la valeur de l’objet WriteableBitmap.

7. Examinez le code de la méthode genererDonneesGraph :

private void genererDonneesGraph(byte[] donnees) { int a = largeurPixel / 2; int b = a * a; int c = hauteurPixel / 2; for (int x = 0; x < a; x ++) {

Page 19: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

int s = x * x; double p = Math.Sqrt(b - s); for (double i = -p; i < p; i += 3) { double r = Math.Sqrt(s + i * i) / a; double q = (r - 1) * Math.Sin(24 * r); double y = i / 3 + (q * c); tracerXY(donnees, (int)(-x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); tracerXY(donnees, (int)(x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); } } }

Cette méthode effectue une série de calculs pour tracer les points d’un graphique plutôt complexe (le calcul en lui-même n’a pas d’importance, il se contente de générer un graphique qui attire l’œil !). Au fur et à mesure du calcul de chaque point, la méthode tracerXY est appelée pour définir les octets appropriés du tableau donnees qui correspond à ces points. Les points du graphique se reflètent autour de l’axe X, si bien que la méthode tracerXY est appelée deux fois pour chaque calcul : une fois pour la valeur positive de la coordonnée X, et une fois pour la valeur négative.

8. Examinez la méthode tracerXY :

private void tracerXY(byte[] donnees, int x, int y) { donnees[x + y * largeurPixel] = 0xFF; }

C’est une méthode simple qui définit l’octet approprié dans le tableau donnees qui correspond aux coordonnées X et Y passés en paramètres. La valeur 0xFF indique que le pixel correspondant doit être défini à blanc lorsque le graphique est affiché. Tous les pixels qui ne sont pas définis sont affichés en noir.

9. Dans le menu Déboguer, cliquez sur Démarrer sans débogage pour générer et exécuter l’application.

10. Lorsque la fenêtre Graph Demo apparaît, cliquez sur Tracer graphique, et attendez.

Soyez patient s’il vous plaît. L’application met plusieurs secondes à générer et afficher le graphique. L’image suivante illustre le graphique. Notez la

Page 20: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

valeur du label Durée (ms) dans l’image suivante. Dans ce cas, l’application a mis 4 566 millisecondes (ms) pour tracer le graphique.

Note L’application a été exécutée sur un ordinateur avec 2 Go de mémoire et un Intel® Core 2 Duo E6600 tournant à 2,40 GHz. Vos temps peuvent varier si vous utilisez un processeur différent ou une quantité de mémoire différente. De plus, vous pouvez remarquer que cela semble plus long au départ pour afficher le graphique que le temps affiché. Cela s’explique par le fait que le temps pris pour initialiser les structures de données nécessaires pour afficher le graphique fait partie de la méthode WritePixels du contrôle graphBitmap et se rajoute au temps pris pour calculer les données du graphique. Les exécutions suivantes du programme n’auront pas cette surcharge du processeur.

11. Cliquez sur Tracer graphique une fois encore, et notez la durée. Répétez cette action plusieurs fois pour obtenir une valeur moyenne.

12. Sur le bureau, faites un clic droit sur une zone vide de la barre de tâches, puis dans le menu déroulant, cliquez sur Démarrer le Gestionnaire de tâches.

Note Sous Windows Vista, la commande du menu déroulant s’appelle Gestionnaire de tâches.

13. Dans le Gestionnaire de tâches de Windows, cliquez sur l’onglet Performance.

14. Retournez dans la fenêtre Graph Demo puis cliquez sur Tracer graphique.

15. Dans le Gestionnaire de tâches de Windows, notez la valeur maximum de l’usage du CPU lorsque le graphique est généré. Vos résultats varieront, mais

Page 21: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

sur un processeur dual-core, l’utilisation du CPU se situera probablement aux alentours de 50–55 pour cent, comme cela est illustré dans l’image suivante. Sur une machine quad-core, l’utilisation du CPU sera probablement inférieure à 30 pour cent.

16. Retournez dans la fenêtre Graph Demo, puis cliquez sur Tracer graphique à nouveau. Notez la valeur de l’usage du CPU dans le Gestionnaire de tâches de Windows. Répétez cette action plusieurs fois pour obtenir une valeur moyenne.

17. Fermez la fenêtre Graph Demo et réduisez le Gestionnaire de tâches de Windows.

Vous avez à présent un point de comparaison du temps que l’application met à accomplir ses calculs. Il est cependant clair, si l’on se base sur l’utilisation du CPU affichée par le Gestionnaire de tâches de Windows, que l’application ne réalise pas le plein usage des ressources disponibles du processeur. Sur une machine dual-core, elle utilise un peu plus de la moitié de la puissance du CPU, et sur une machine quad-core elle n’emploie qu’un peu plus du quart du CPU. Ce phénomène se produit car l’application est mono-thread, et dans une application Windows, un thread unique ne peut employer qu’un seul cœur d’un processeur multicœurs. Pour répartir la charge sur tous les cœurs disponibles, vous devez diviser l’application en tâches et organiser chacune des tâches pour qu’elle soit exécutée par thread séparé tournant sur un cœur différent.

Page 22: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

Modification de l’application GraphDemo en utilisant des threads parallèles

1. Retournez dans Visual Studio 2010, et affichez le fichier GraphWindow.xaml.cs dans la fenêtre Code s’il n’est pas déjà ouvert.

2. Examinez la méthode genererDonneesGraph.

Si vous réfléchissez bien, le but de cette méthode est de remplir les éléments du tableau donnees. Elle parcourt le tableau grâce à la boucle externe for en se basant sur la variable de contrôle de boucle x, qui est mise en valeur ici en gras :

private void genererDonneesGraph(byte[] donnees) { int a = largeurPixel / 2; int b = a * a; int c = hauteurPixel / 2;

for (int x = 0; x < a; x ++) { int s = x * x; double p = Math.Sqrt(b - s); for (double i = -p; i < p; i += 3) { double r = Math.Sqrt(s + i * i) / a; double q = (r - 1) * Math.Sin(24 * r); double y = i / 3 + (q * c); tracerXY(donnees, (int)(-x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); tracerXY(donnees, (int)(x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); } } }

Le calcul accompli par une itération de cette boucle est indépendant des calculs accomplis par les autres itérations. Par conséquent, il est intéressant de partitionner le travail accompli par cette boucle et d’exécuter des itérations différentes sur un processeur séparé.

3. Modifiez la définition de la méthode genererDonneesGraph pour accepter deux paramètres supplémentaires int appelés partitionDebut et partitionFin, comme illustré en gras :

private void genererDonneesGraph(byte[] donnees, int partitionDebut, int partitionFin)

Page 23: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

{ ... }

4. Dans la méthode genererDonneesGraph, modifiez la boucle externe for pour aller des valeurs partitionDebut à partitionFin, comme illustré en gras :

private void genererDonneesGraph(byte[] donnees, int partitionDebut, int partitionFin) { ... for (int x = partitionDebut; x < partitionFin; x ++) { ... } }

5. Dans la fenêtre Code, ajoutez l’instruction using suivante à la liste en haut du fichier GraphWindow.xaml.cs :

using System.Threading.Tasks;

6. Dans la méthode tracerButton_Click, mettez en commentaire l’instruction qui appelle la méthode genererDonneesGraph et ajoutez l’instruction en gras qui crée un objet Task avec l’objet TaskFactory par défaut et démarre son exécution :

... Stopwatch montre = Stopwatch.StartNew(); // genererDonneesGraph(donnees); Task premiere = Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0, largeurPixel / 4)); ...

La tâche exécute le code spécifié par l’expression lambda. Les valeurs des paramètres partitionDebut et partitionFin indiquent que l’objet Task calcule les données de la première moitié du graphique (les données du graphique complet se composent des points tracés pour les values comprises entre 0 et largeurPixel / 2).

7. Ajoutez une autre instruction qui crée et exécute un deuxième objet Task sur un autre thread, comme cela est indiqué en gras :

...

Task premiere = Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0,

Page 24: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

largeurPixel / 4)); Task deuxieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 4, largeurPixel / 2)); ...

Cet objet Task invoque la méthode generateGraph et calcule les données des valeurs comprises entre largeurPixel / 4 et largeurPixel / 2.

8. Ajoutez l’instruction suivante qui attend que les deux objets Task aient fini leur travail avant de continuer :

Task.WaitAll(premiere, deuxieme);

9. Dans le menu Déboguer, cliquez sur Démarrer sans débogage pour générer et exécuter l’application.

10. Affichez le Gestionnaire de tâches de Windows, et cliquez sur l’onglet Performance s’il n’est pas affiché.

11. Retournez dans la fenêtre Graph Demo, et cliquez sur Tracer graphique. Dans le Gestionnaire de tâches de Windows, notez la valeur maximum de l’utilisation du CPU lorsque le graphique est généré. Lorsque le graphique apparaît dans la fenêtre Graph Demo, enregistrez le temps mis pour générer le graphique. Répétez cette action plusieurs fois pour obtenir une valeur moyenne.

12. Fermez la fenêtre Graph Demo, et réduisez le Gestionnaire de tâches de Windows.

Cette fois-ci, vous devriez constater que l’application s’exécute beaucoup plus vite que précédemment. Sur mon ordinateur, la durée a chuté à 2 682 millisecondes, ce qui constitue une réduction de près de 40 %. En outre, vous devriez voir que l’application utilise plus les cœurs du CPU. Sur une machine dual-core, l’utilisation du CPU atteint 100 pour cent. Si vous avez un ordinateur quad-core, l’utilisation du CPU ne sera pas aussi élevée. Cela est dû au fait que deux des cœurs ne seront pas occupés. Pour rectifier cela et réduire encore le temps d’exécution, ajoutez deux objets Task supplémentaires et divisez le travail en quatre parties dans la méthode tracerButton_Click, comme cela est indiqué ici en gras :

... Task premiere = Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0, largeurPixel / 8)); Task deuxieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 8, largeurPixel / 4));

Page 25: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Task troisieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 4, largeurPixel * 3 / 8)); Task quatrieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel * 3 / 8, largeurPixel / 2)); Task.WaitAll(premiere, deuxieme, troisieme, quatrieme); ...

Si vous n’avez qu’un processeur dual-core, vous pouvez quand même tenter cette modification, et vous devriez quand même noter un effet bénéfique sur le temps d’exécution. Cela est principalement dû à l’efficacité de la TPL et des algorithmes du .NET Framework qui optimisent la manière dont les threads de chaque tâche sont planifiés.

Abstraction des tâches avec la classe Parallel En utilisant la classe Task, vous bénéficiez d’un contrôle total sur le nombre de tâches que votre application crée. Cependant, vous devez modifier la conception de l’application pour adapter l’utilisation des objets Task. Vous devez aussi ajouter du code pour synchroniser les opérations ; l’application ne peut afficher le graphique que lorsque toutes les tâches sont terminées. Dans une application complexe, la synchronisation des tâches peut devenir un processus problématique dans lequel il est facile de commettre des erreurs.

La classe Parallel de la TPL permet de paralléliser certaines constructions de programmation courantes sans nécessiter la modification de la conception de l’application. En interne, la classe Parallel crée son propre ensemble d’objets Task, et elle synchronise ces tâches automatiquement quand elles sont terminées. La classe Parallel est située dans l’espace de noms System.Threading.Tasks et fournit un jeu réduit de méthodes statiques que vous pouvez utiliser pour indiquer que le code doit être exécuté en parallèle si possible. Voici la liste de ces méthodes :

■ Parallel.For Vous pouvez utiliser cette méthode à la place d’une commande C# for. Elle définit une boucle dans laquelle les itérations peuvent s’exécuter en parallèle en utilisant des tâches. Cette méthode est lourdement surchargée (il y a neuf variantes), mais le principe général est identique pour chaque version ; on spécifie une valeur de départ, une valeur de fin, et une référence à une méthode qui accepte un paramètre integer. La méthode est exécutée pour chaque valeur comprise entre la valeur de départ et la valeur de fin moins un, et le paramètre est rempli avec un entier qui spécifie la

Page 26: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

valeur en cours. L’exemple suivant illustre une simple boucle for qui accomplit chaque itération en séquence :

for (int x = 0; x < 100; x++) { // Accomplit le traitement de la boucle }

En fonction du traitement accompli par le corps de la boucle, vous pouvez remplacer cette boucle par une construction Parallel.For qui peut réaliser des itérations en parallèle, de la manière suivante :

Parallel.For(0, 100, performLoopProcessing); ... private void performLoopProcessing(int x) { // Accomplit le traitement de la boucle }

Les surchargements de la méthode Parallel.For permettent de fournir des données locales qui sont privées pour chaque thread, de spécifier différentes options pour créer les tâches exécutées par la méthode For, et de créer un objet ParallelLoopState qui peut être utilisé pour passer des informations d’état à d’autres itérations concurrentes de la boucle (l’objet ParallelLoopState est décrit plus loin dans ce chapitre).

■ Parallel.ForEach<T> Vous pouvez utiliser cette méthode à la place d’une commande C# foreach. Comme la méthode For, ForEach définit une boucle dans laquelle les itérations peuvent s’exécuter en parallèle. On spécifie une collection qui implémente l’interface générique IEnumerable<T> et une référence à une méthode qui accepte un seul paramètre de type T. La méthode est exécutée pour chaque élément de la collection, et l’élément est passé en paramètre à la méthode. Les surchargements sont disponibles et permettent de fournir des données de thread local privé et de spécifier des options pour créer les tâches exécutées par la méthode ForEach.

■ Parallel.Invoke Vous pouvez utiliser cette méthode pour exécuter un ensemble d’appels de méthodes sans paramètre sous la forme de tâches parallèles. On spécifie une liste d’appels de méthodes déléguées (ou d’expressions lambda) qui n’acceptent pas de paramètre et ne retourne pas de valeur. Chaque appel de méthode peut être exécuté sur un thread séparé, dans n’importe quel ordre. Par exemple, le code suivant effectue une série d’appels de méthodes :

Page 27: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

faireTravail(); faireAutreTravail(); faireEncoreAutreTravail();

Vous pouvez remplacer ces commandes par le code suivant qui invoque ces méthodes en utilisant une série de tâches :

Parallel.Invoke( faireTravail, faireAutreTravail, faireEncoreAutreTravail );

Vous devez garder à l’esprit que le .NET Framework détermine le degré réel de parallélisme approprié à l’environnement et à la charge de travail de l’ordinateur. Par exemple, si vous utilisez Parallel.For pour implémenter une boucle qui accomplit 1 000 itérations, le .NET Framework ne crée pas nécessairement 1 000 tâches concurrentes (à moins que vous n’ayez un processeur particulièrement puissant doté de mille cœurs). Au lieu de cela, le .NET Framework crée ce qu’il considère comme le nombre optimal de tâches qui est un compromis entre les ressources disponibles et la nécessité de garder les processeurs actifs. Une seule tâche peut accomplir plusieurs itérations, et les tâches se coordonnent entre elles pour déterminer quelles itérations chaque tâche va accomplir. Cela a pour conséquence que vous ne pouvez pas garantir l’ordre dans lequel les itérations sont exécutées, si bien que vous devez vous assurer qu’il n’y a pas de dépendances entre les itérations ; dans le cas contraire, vous pouvez obtenir des résultats inattendus, comme vous le verrez plus tard dans ce chapitre.

Dans l’exercice suivant, vous allez retourner à la version originale de l’application GraphData et utiliser la classe Parallel pour accomplir des opérations de manière concurrente.

Utilisation de la classe Parallel pour paralléliser les opérations de l’application GraphData

1. Dans Visual Studio 2010, ouvrez la solution GraphDemo, située dans le dossier \Visual C Sharp Etape par étape\Chapitre 27\GraphDemo Utilisant La Classe Parallel de votre dossier Documents.

Ceci est une copie de l’application originale GraphDemo qui n’utilise pas les tâches pour l’instant.

Page 28: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

2. Dans l’Explorateur de solutions, dans le projet GraphDemo, développez le nœud GraphWindow.xaml, puis faites un double clic sur GraphWindow.xaml.cs pour afficher le code du formulaire dans la fenêtre Code.

3. Ajoutez l’instruction using suivante à la liste en haut du fichier :

using System.Threading.Tasks;

4. Recherchez la méthode genererDonneesGraph. Elle ressemble à ceci :

private void genererDonneesGraph(byte[] donnees) { int a = largeurPixel / 2; int b = a * a; int c = hauteurPixel / 2; for (int x = 0; x < a; x++) { int s = x * x; double p = Math.Sqrt(b - s); for (double i = -p; i < p; i += 3) { double r = Math.Sqrt(s + i * i) / a; double q = (r - 1) * Math.Sin(24 * r); double y = i / 3 + (q * c); tracerXY(donnees, (int)(-x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); tracerXY(donnees, (int)(x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); } } }

La boucle externe for qui parcourt les valeurs de la variable integer x est un parfait candidat pour la parallélisation. Vous pouvez aussi envisager la boucle interne basée sur la variable i, mais cette boucle demande plus d’effort pour la paralléliser en raison du type de i (les méthodes de la classe Parallel attendent que la variable de contrôle soit de type integer). En outre, si vous avez des boucles imbriquées, comme c’est le cas dans ce code, il est conseillé de paralléliser d’abord les boucles externes, puis de voir si les performances de l’application sont suffisantes. Si ce n’est pas le cas, rabattez-vous sur les boucles imbriquées et parallélisez-les en allant des boucles externes vers les boucles internes, et en testant les performances après chaque modification de boucle. Vous constaterez que dans de nombreux cas la parallélisation des boucles externes a le plus d’effet sur les performances, alors que la

Page 29: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

modification des boucles internes a un effet marginal.

5. Déplacez le code du corps de la boucle for, et créez une nouvelle méthode privée void appelée calculerDonnees avec ce code. La méthode calculerDonnees doit prendre un paramètre integer appelé x et un tableau byte appelé donnees. Déplacez aussi les instructions qui déclarent les variables locales a, b, et c de la méthode genererDonneesGraph au début de la méthode calculerDonnees. Le code suivant montre la méthode genererDonneesGraph avec le code supprimé et la méthode calculerDonnees (ne compilez pas ce code pour le tester pour l’instant) :

private void genererDonneesGraph(byte[] donnees) { for (int x = 0; x < a; x++) { } } private void calculerDonnees(int x, byte[] donnees) { int a = largeurPixel / 2; int b = a * a; int c = hauteurPixel / 2; int s = x * x; double p = Math.Sqrt(b - s); for (double i = -p; i < p; i += 3) { double r = Math.Sqrt(s + i * i) / a; double q = (r - 1) * Math.Sin(24 * r); double y = i / 3 + (q * c); tracerXY(donnees, (int)(-x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); tracerXY(donnees, (int)(x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); } }

6. Dans la méthode genererDonneesGraph, modifiez la boucle for pour une instruction qui appelle la méthode statique Parallel.For, comme cela est indiqué en gras ici :

private void genererDonneesGraph(byte[] donnees) { Parallel.For (0, largeurPixel / 2, (int x) => { calculerDonnees(x, donnees); });

Page 30: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

}

Ce code est l’équivalent parallèle de la boucle originale for. Il parcourt les valeurs de 0 à largeurPixel / 2 – 1 inclus. Chaque invocation s’exécute en utilisant une tâche (chaque tâche peut exécuter plus d’une itération). La méthode Parallel.For ne se termine que lorsque toutes les tâches qu’elle a créées ont achevé leur travail. Rappelez-vous que la méthode Parallel.For s’attend à ce que le dernier paramètre soit une méthode qui n’accepte qu’un seul paramètre integer. Elle appelle cette méthode en passant l’indice de boucle en cours comme paramètre. Dans cet exemple, la méthode calculerDonnees ne correspond pas à la signature nécessaire car elle prend deux paramètres : un integer et un tableau de types byte. Pour cette raison, le code utilise une expression lambda pour définir une méthode anonyme qui la signature appropriée et qui agit comme un adaptateur qui appelle la méthode calculerDonnees avec les bons paramètres.

7. Dans le menu Déboguer, cliquez sur Démarrer sans débogage pour générer et exécuter l’application.

8. Affichez le Gestionnaire de tâches de Windows, et cliquez sur l’onglet Performance s’il n’est pas déjà affiché.

9. Retournez dans la fenêtre Graph Demo, et cliquez sur Tracer graphique. Dans le Gestionnaire de tâches de Windows, notez la valeur maximum d’utilisation du CPU lorsque le graph est généré. Quand le graphique apparaît dans la fenêtre Graph Demo, enregistrez le temps mis pour générer le graphique. Répétez cette action plusieurs fois pour obtenir une valeur moyenne.

10. Fermez la fenêtre Graph Demo, et réduisez le Gestionnaire de tâches de Windows.

Vous devriez noter que l’application s’exécute à une vitesse comparable à la version précédente qui utilisait des objets Task (et peut-être un peu plus rapidement, en fonction du nombre de CPU dont vous disposez), et que l’utilisation du CPU culmine à 100 pourcent.

Quand faut-il ne pas utiliser la classe Parallel Vous devez être conscient qu’en dépit des apparences et du travail considérable accompli par l’équipe de développement de Visual Studio, la classe Parallel n’accomplit pas des miracles ; vous ne pouvez pas l’utiliser sans réfléchir et vous ne devez pas vous attendre à ce que vos applications s’exécutent beaucoup plus vite

Page 31: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

et produisent les mêmes résultats. Le but de la classe Parallel est de paralléliser les zones indépendantes de votre code qui sont liées au matériel.

Les points importants du paragraphe précédent sont liés au matériel et indépendant. Si votre code n’est pas lié au matériel, le fait de le paralléliser n’améliorera pas ses performances. L’exercice suivant vous montre comment juger de l’opportunité d’employer la construction Parallel.Invoke pour réaliser des appels de méthodes en parallèle.

Opportunité d’utilisation de Parallel.Invoke

1. Retournez dans Visual Studio 2010, et affichez le fichier GraphWindow.xaml.cs dans la fenêtre Code s’il n’est pas déjà ouvert.

2. Examinez la méthode calculerDonnees.

La boucle interne for contient les instructions suivantes :

tracerXY(donnees, (int)(-x + (largeurPixel / 2)), (int)(y + (largeurPixel / 2))); tracerXY(donnees, (int)(x + (largeurPixel / 2)), (int)(y + (largeurPixel / 2)));

Ces deux instructions définissent les octets du tableau donnees qui correspondent aux points spécifiés par les deux paramètres passés à la méthode. Rappelez-vous que les points du graphique se reflètent autour de l’axe X, si bien que la méthode tracerXY est appelée pour la valeur positive de X, mais aussi pour sa valeur négative. Ces deux instructions semblent de bons candidats pour la parallélisation car elles définissent des octets différents dans le tableau donnees et leur ordre d’exécution n’a pas d’importance.

3. Modifiez ces deux instructions et encapsulez-les dans un appel de méthode Parallel.Invoke, comme cela est indiqué ci-dessous. Vous noterez que les deux appels sont à présent encapsulés dans des expressions lambda, et que le point-virgule à la fin du premier appel à tracerXY est remplacé par une virgule, et que le point-virgule à la fin du deuxième appel à tracerXY a été supprimé car ces instructions sont maintenant une liste de paramètres :

Parallel.Invoke( () => tracerXY(donnees, (int)(-x + (largeurPixel / 2)), (int)(y + (largeurPixel / 2))),

Page 32: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

() => tracerXY(donnees, (int)(x + (largeurPixel / 2)), (int)(y + (largeurPixel / 2))) );

4. Dans le menu Déboguer, cliquez sur Démarrer sans débogage pour générer et exécuter l’application.

5. Dans la fenêtre Graph Demo, cliquez sur Tracer graphique. Enregistrez le temps mis pour générer le graphique. Répétez cette action plusieurs fois pour obtenir une valeur moyenne.

De manière assez inattendue, vous devriez trouver que l’application est beaucoup plus longue à s’exécuter. Elle peut être jusqu’à 20 fois plus lente que précédemment.

6. Fermez la fenêtre Graph Demo.

Vous devez sans doute vous poser la question de savoir ce qui se passe. Pourquoi l’application est-elle tellement ralentie ? La réponse se trouve dans la méthode tracerXY. Si vous examinez à nouveau cette méthode, vous verrez que c’est très simple :

private void tracerXY(byte[] donnees, int x, int y) { donnees[x + y * largeurPixel] = 0xFF; }

Il y a très peu de chose dans cette méthode qui met du temps à s’exécuter, et il ne s’agit absolument pas de code lié au matériel. En fait, le code est tellement simple que la surcharge de création d’une tâche, l’exécution de cette tâche sur un thread séparé, et l’attente de la fin de cette tâche sont bien plus importantes que le coût de l’exécution de cette méthode directement. La surcharge supplémentaire ne peut coûter que quelques millisecondes à chaque appel de méthode, mais vous devez garder à l’esprit que cette méthode est exécutée un très grand nombre de fois ; l’appel de méthode est situé dans une boucle imbriquée et il est exécuté des milliers de fois, si bien que toutes ces petites surcharges finissent par faire une somme. En règle générale, il faut utiliser Parallel.Invoke seulement quand cela en vaut la peine et réserver Parallel.Invoke pour les opérations qui sollicitent le matériel de manière intensive.

Comme cela est mentionné plus haut dans ce chapitre, l’autre point important dans l’utilisation de la classe Parallel est que les opérations doivent être indépendantes. Par exemple, si vous tentez d’utiliser Parallel.For pour paralléliser

Page 33: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

une boucle dans laquelle les itérations ne sont pas indépendantes, les résultats seront imprévisibles. Pour voir ce que cela signifie, examinez le programme suivant :

using System; using System.Threading; using System.Threading.Tasks; namespace BoucleParallele { class Program { private static int accumulateur = 0; static void Main(string[] args) { for (int i = 0; i < 100; i++) { AjouterAAccumulateur(i); } Console.WriteLine("Accumulateur est {0}", accumulateur); } private static void AjouterAAccumulateur(int donnees) { if ((accumulateur % 2) == 0) { accumulateur += donnees; } else { accumulateur -= donnees; } } } }

Ce programme parcourt les valeurs de 0 à 99 et appelle la méthode AjouterAAccumulateur avec chacune de ces valeurs. La méthode AjouterAAccumulateur examine la valeur en cours de la variable accumulateur, et si elle est paire elle ajoute la valeur du paramètre à la variable accumulateur ; dans le cas contraire, elle soustrait la valeur du paramètre. À la fin du programme, le résultat est affiché. Vous trouverez cette application dans la solution BoucleParallele, située dans le dossier \Visual C Sharp Etape par étape\Chapitre 27\BoucleParallele de votre dossier Documents. Si vous exécutez ce programme, il

Page 34: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

doit afficher une valeur de –100.

Pour augmenter le degré de parallélisme de cette application simple, vous pouvez être tenté de remplacer la boucle for de la méthode Main par Parallel.For :

static void Main(string[] args) { Parallel.For (0, 100, AjouterAAccumulateur); Console.WriteLine("Accumulateur est {0}", accumulateur); }

Il n’y a cependant aucune garantie que les tâches créées pour exécuter les différentes invocations de la méthode AjouterAAccumulateur s’exécuteront dans une séquence spécifique (ce code n’est pas non plus thread-safe car plusieurs threads exécutant les tâches peuvent tenter de modifier la variable accumulateur en même temps). La valeur calculée par la méthode AjouterAAccumulateur dépend du maintien de la séquence, si bien que le résultat de la modification est que l’application peut à présent générer des valeurs différentes chaque fois qu’elle s’exécute. Dans cet exemple simple, vous ne verrez peut-être aucune différence dans le calcul de la valeur car la méthode AjouterAAccumulateur s’exécute très rapidement et le .NET Framework peut choisir d’exécuter chaque invocation séquentiellement en utilisant le même thread. Cependant, si vous effectuez le changement suivant (indiqué en gras) dans la méthode AjouterAAccumulateur, vous obtiendrez des résultats différents :

private static void AjouterAAccumulateur(int donnees) { if ((accumulateur % 2) == 0) { accumulateur += donnees; Thread.Sleep(10); // attente de 10 millisecondes } else { accumulateur -= donnees; } }

La méthode Thread.Sleep provoque simplement l’arrêt du thread en cours pendant la durée spécifiée. Cette modification stimule le thread, en accomplissant un traitement supplémentaire qui affecte la manière dont le .NET Framework planifie les tâches, qui s’exécutent maintenant sur des threads différents, ce qui provoque une séquence différente.

Page 35: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

En règle générale, il faut utiliser Parallel.For et Parallel.ForEach seulement quand vous pouvez garantir que chaque itération de la boucle est indépendante, et vous devez tester votre code minutieusement. Ces considérations s’appliquent également à Parallel.Invoke ; utilisez cette construction pour effectuer des appels de méthodes seulement si elles sont indépendantes et si l’application ne dépend pas de leur exécution dans un certain ordre.

Retour d’une valeur à partir d’une tâche Jusqu’à présent, tous les exemples que nous avons étudiés utilisaient un objet Task pour exécuter le code qui accomplit une partie du travail, mais ne retourne pas de valeur. Vous pouvez cependant avoir envie d’exécuter une méthode qui calcule un résultat. La TPL inclut une variante générique de la classe Task, Task<TResult>, que vous pouvez utiliser à cet effet.

On crée et exécute un objet Task<TResult> de la même manière qu’un objet Task. La principale différence est que la méthode exécutée par l’objet Task<TResult> retourne une valeur et que l’on spécifie le type de cette valeur de retour, en tant que paramètre type, T, de l’objet Task. Par exemple, la méthode calculerValeur illustrée dans le code suivant retourne une valeur integer. Pour invoquer cette méthode en utilisant une tâche, on crée un objet Task<int> et on appelle ensuite la méthode Start. On obtient la valeur retournée par la méthode en interrogeant la propriété Result de l’objet Task<int>. Si la tâche n’a pas terminé l’exécution de la méthode et que le résultat n’est pas encore disponible, la propriété Result bloque l’appelant. Cela signifie que vous n’avez pas à accomplir vous-même de synchronisation, et que vous savez que lorsque la propriété Result retourne une valeur la tâche a terminé son travail.

Task<int> tacheCalculerValeur = new Task<int>(() => calculerValeur(...)); tacheCalculerValeur.Start(); // Invocation de la méthode calculerValeur ... int donneesCalculees = tacheCalculerValeur.Result; // On bloque jusqu’à ce que tacheCalculerValeur soit terminée ... private int calculerValeur(...) { int uneValeur; // Calcul et remplissage de uneValeur ... return uneValeur;

Page 36: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

}

Bien entendu, vous pouvez aussi utiliser la méthode StartNew d’un objet TaskFactory pour créer un objet Task<TResult> et démarrer son exécution. L’exemple de code suivant montre comment utiliser l’objet par défaut TaskFactory d’un objet Task<int> pour créer et exécuter une tâche qui invoque la méthode calculerValeur :

Task<int> tacheCalculerValeur = Task<int>.Factory.StartNew(() => calculerValeur(...)); ...

Pour simplifier un peu votre code (et prendre en charge les tâches qui retournent des types anonymes), la classe TaskFactory fournit des surcharges génériques de la méthode StartNew et peut déduite le type retourné par la méthode exécutée par une tâche. En outre, la classe Task<TResult> hérite de la classe Task. Cela signifie que vous pouvez réécrire l’exemple précédent de la manière suivante :

Task tacheCalculerValeur = Task.Factory.StartNew(() => calculerValeur(...)); ...

Le prochain exercice offre un exemple plus détaillé : vous allez restructurer l’application GraphDemo afin d’utiliser un objet Task<TResult>. Bien que cet exercice semble un peu scolaire, la technique qui est ici illustrée pourra se révéler utile dans de nombreuses situations réelles.

Modification de l’application GraphDemo afin d’utiliser un objet Task<TResult>

1. Dans Visual Studio 2010, ouvrez la solution GraphDemo, situé dans le dossier \Visual C Sharp Etape par étape\Chapitre 27\GraphDemo Utilisant Taches Qui Retournent Résultats de votre dossier Documents.

Il s’agit d’une copie de l’application GraphDemo qui crée un ensemble de quatre tâches que vous avez étudiée dans un exercice précédent.

2. Dans l’Explorateur de solutions, dans le projet GraphDemo, développez le nœud GraphWindow.xaml, puis faites un double clic sur GraphWindow.xaml.cs pour afficher le code du formulaire dans la fenêtre Code.

3. Recherchez la méthode tracerButton_Click. C’est la méthode qui s’exécute lorsque l’utilisateur clique sur le bouton Tracer graphique du formulaire.

Page 37: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Actuellement, elle crée un ensemble d’objets Task pour accomplir les différents calculs nécessaires et générer les données du graphique, et elle attend que les objets Task se terminent avant d’afficher les résultats dans le contrôle Image du formulaire.

4. En dessous de la méthode tracerButton_Click, ajoutez une nouvelle méthode appelée obtenirDonneesPourGraph. Cette méthode doit prendre un paramètre integer appelé tailleDonnees et retourne un tableau byte, comme cela est illustré dans le code suivant :

private byte[] obtenirDonneesPourGraph(int tailleDonnees) { }

Vous allez ajouter du code à cette méthode pour générer les données du graphique dans un tableau byte et retourner ce tableau à l’appelant. Le paramètre tailleDonnees spécifie la taille du tableau.

5. Déplacez l’instruction qui crée le tableau donnees de la méthode tracerButton_Click dans la méthode obtenirDonneesPourGraph comme cela est indiqué en gras :

private byte[] obtenirDonneesPourGraph(int tailleDonnees) { byte[] donnees = new byte[tailleDonnees]; }

6. Déplacez le code qui crée, exécute et attend que les objets Task remplissent le tableau donnees de la méthode tracerButton_Click dans la méthode obtenirDonneesPourGraph, et ajoutez une instruction return à la fin de la méthode qui retourne le tableau donnees à l’appelant. Le code complet de la méthode obtenirDonneesPourGraph doit ressembler à ceci :

private byte[] obtenirDonneesPourGraph(int tailleDonnees) { byte[] donnees = new byte[tailleDonnees]; Task premiere = Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0, largeurPixel / 8)); Task deuxieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 8, largeurPixel / 4)); Task troisieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 4, largeurPixel * 3 / 8)); Task quatrieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel * 3 / 8, largeurPixel / 2)); Task.WaitAll(premiere, deuxieme, troisieme, quatrieme);

Page 38: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

return donnees; }

Astuce Vous pouvez remplacer le code qui crée les tâches et attend leur achèvement par la construction Parallel.Invoke suivante :

Parallel.Invoke( () => Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0, largeurPixel / 8)) () => Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 8, largeurPixel / 4)), () => Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 4, largeurPixel * 3 / 8)), () => Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel * 3 / 8, largeurPixel / 2)) );

7. Dans la méthode tracerButton_Click, après l’instruction qui crée la variable Stopwatch utilisée pour chronométrer les tâches, ajoutez la commande indiquée en gras qui crée un objet Task<byte[]> appelé obtenirDonneesTache et utilise cet objet pour exécuter la méthode obtenirDonneesPourGraph. Cette méthode retourne un tableau byte, si bien que le type de la tâche est Task<byte []>. L’appel de la méthode StartNew référence une expression lambda qui invoque la méthode obtenirDonneesPourGraph et passe la variable tailleDonnees en paramètre à cette méthode.

private void tracerButton_Click(objet sender, RoutedEventArgs e) { ... Stopwatch montre = Stopwatch.StartNew(); Task<byte[]> obtenirDonneesTache = Task<byte[]>.Factory.StartNew(() => obtenirDonneesPourGraph(tailleDonnees)); ... }

8. Après la création et le démarrage de l’objet Task<byte []>, ajoutez l’instruction illustrée en gras qui examine la propriété Result afin de récupérer le tableau donnees retourné par la méthode obtenirDonneesPourGraph dans une variable locale de tableau byte appelée donnees. Rappelez-vous que la propriété Result bloque l’appelant tant que la tâche n’est pas terminée, si bien que vous n’avez pas besoin d’attendre la fin de la tâche.

private void tracerButton_Click(objet sender, RoutedEventArgs e) {

Page 39: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

... Task<byte[]> obtenirDonneesTache = Task<byte[]>.Factory.StartNew(() => obtenirDonneesPourGraph(tailleDonnees)); byte[] donnees = obtenirDonneesTache.Result; ... }

Note Il peut sembler un peu étrange de créer une tâche, puis d’attendre immédiatement son achèvement avant de faire quelque chose d’autre car cela ne fait que rajouter de la surcharge à l’application. Vous verrez cependant dans la prochaine section pourquoi cette approche a été adoptée.

9. Vérifiez que le code complet de la méthode tracerButton_Click ressemble à ceci :

private void tracerButton_Click(objet sender, RoutedEventArgs e) { if (graphBitmap == null) { graphBitmap = new WriteableBitmap(largeurPixel, hauteurPixel, dpiX, dpiY, PixelFormats.Gray8, null); } int octetsParPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8; int stride = octetsParPixel * largeurPixel; int tailleDonnees = stride * hauteurPixel; Stopwatch montre = Stopwatch.StartNew(); Task<byte[]> obtenirDonneesTache = Task<byte[]>.Factory.StartNew(() => obtenirDonneesPourGraph(tailleDonnees)); byte[] donnees = obtenirDonneesTache.Result; duree.Content = string.Format("Durée (ms) : {0}", montre.ElapsedMilliseconds); graphBitmap.WritePixels(new Int32Rect(0, 0, largeurPixel, hauteurPixel), donnees, stride, 0); graphImage.Source = graphBitmap; }

10. Dans le menu Déboguer, cliquez sur Démarrer sans débogage pour générer et exécuter l’application.

11. Dans la fenêtre Graph Demo, cliquez sur Tracer graphique. Vérifiez que le graphique est généré comme auparavant et que le temps d’affichage est similaire aux valeurs précédentes (le temps affiché peut être un peu plus long

Page 40: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

car le tableau donnees est à présent créé par la tâche, alors qu’auparavant il était créé avant que la tâche ne démarre son exécution).

12. Fermez la fenêtre Graph Demo.

Utilisation conjointe des tâches et des threads d’interface utilisateur

La section « Pourquoi gérer le mode multitâche par un traitement parallèle ? », au début de ce chapitre, a mis en lumière les deux principales raisons de l’utilisation du multitâche dans une application : l’amélioration du débit et l’augmentation de la réactivité. La TPL est certainement d’un grand secours dans l’amélioration du débit, mais vous devez être conscient que l’utilisation de la seule TPL n’est pas l’ultime solution pour augmenter la réactivité, particulièrement dans une application qui offre une interface utilisateur graphique. Dans l’application GraphDemo utilisée comme base des exercices de ce chapitre, bien que le temps mis pour générer les données du graphique soit réduit par l’utilisation des tâches, l’application montre elle-même les symptômes classiques de nombreuses interfaces utilisateur graphiques qui accomplissent des calculs sollicitant le processeur : elle ne réagit pas aux sollicitations de l’utilisateur lors des calculs. Par exemple, si vous exécutez l’application GraphDemo de l’exercice précédent, cliquez sur le bouton Tracer graphique, puis tentez de déplacer la fenêtre Graph Demo en cliquant sur la barre de titre ; vous constaterez qu’elle ne bouge pas tant que les différentes tâches utilisées pour générer le graphique ne sont pas terminées et que le graphique n’est pas affiché.

Dans une application professionnelle, vous devez vous assurer que l’on peut toujours utiliser votre application même si certaines parties sont occupées à accomplir d’autres tâches. C’est à ce moment-là que vous devez utiliser des threads ainsi que des tâches.

Au chapitre 23, vous avez vu comment les éléments qui constituent une interface utilisateur graphique d’une application WPF s’exécutent tous dans le thread d’interface utilisateur. Cela permet de garantir la cohérence et la sécurité, et cela empêche deux threads de corrompre les structures de données internes utilisées par WPF pour afficher l’interface utilisateur. Rappelez-vous que l’on peut aussi utiliser l’objet WPF Dispatcher pour mettre en file d’attente les demandes du

Page 41: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

thread d’interface utilisateur, et que ces demandes peuvent mettre à jour l’interface utilisateur. Le prochain exercice revient sur l’objet Dispatcher et montre comment l’utiliser pour implémenter une solution réactive conjointement avec des tâches qui garantissent le meilleur débit disponible.

Amélioration de la réactivité de l’application GraphDemo

1. Retournez dans Visual Studio 2010, et affichez le fichier GraphWindow.xaml.cs dans la fenêtre Code si elle n’est pas déjà ouverte.

2. Ajoutez une nouvelle méthode appelée faireFonctionnerTracerButton au-dessous de la méthode tracerButton_Click. Cette méthode ne doit accepter aucun paramètre et ne doit pas retourner de valeur. Dans les étapes suivantes, vous allez déplacer le code qui crée et exécute les tâches qui génèrent les données du graphique dans cette méthode, et vous allez exécuter cette méthode dans un thread séparé, ce qui laissera le thread d’interface utilisateur libre de gérer la saisie utilisateur.

private void faireFonctionnerTracerButton() { }

3. Déplacez tout le code, excepté l’instruction if qui crée l’objet graphBitmap de la méthode tracerButton_Click dans la méthode faireFonctionnerTracerButton. Notez que certaines de ces instructions tentent d’accéder aux éléments de l’interface utilisateur ; vous allez modifier ces commandes pour utiliser l’objet Dispatcher plus tard dans cet exercice. Les méthodes tracerButton_Click et faireFonctionnerTracerButton doivent ressembler à ceci :

private void tracerButton_Click(objet sender, RoutedEventArgs e) { if (graphBitmap == null) { graphBitmap = new WriteableBitmap(largeurPixel, hauteurPixel, dpiX, dpiY, PixelFormats.Gray8, null); } } private void faireFonctionnerTracerButton() { int octetsParPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8; int stride = octetsParPixel * largeurPixel; int tailleDonnees = stride * hauteurPixel;

Page 42: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

Stopwatch montre = Stopwatch.StartNew(); Task<byte[]> obtenirDonneesTache = Task<byte[]>.Factory.StartNew(() => obtenirDonneesPourGraph(tailleDonnees)); byte[] donnees = obtenirDonneesTache.Result; duree.Content = string.Format("Durée (ms) : {0}", montre.ElapsedMilliseconds); graphBitmap.WritePixels(new Int32Rect(0, 0, largeurPixel, hauteurPixel), donnees, stride, 0); graphImage.Source = graphBitmap; }

4. Dans la méthode tracerButton_Click, après le bloc if, créez un délégué Action appelé actionFaireFonctionerTracerButton qui référence la méthode faireFonctionnerTracerButton, comme cela est indiqué ici en gras :

private void tracerButton_Click(objet sender, RoutedEventArgs e) { ... Action actionFaireFonctionerTracerButton = new Action(faireFonctionnerTracerButton); }

5. Appelez la méthode BeginInvoke sur le délégué actionFaireFonctionerTracerButton. La méthode BeginInvoke du type Action exécute la méthode associée au délégué (en l’occurrence, la méthode faireFonctionnerTracerButton) sur un nouveau thread.

Note Le type Action fournit aussi la méthode Invoke, qui exécute la méthode déléguée sur le thread en cours. Ce n’est pas le comportement que nous souhaitons dans ce cas parce que cela bloque l’interface utilisateur et cela empêche toute réactivité lors de l’exécution de la méthode.

La méthode BeginInvoke accepte des paramètres que vous pouvez utiliser pour mettre en place une notification quand la méthode se termine, ainsi que des données que l’on peut passer à la méthode déléguée. Dans cet exemple, on n’a pas besoin d’être notifié quand la méthode se termine et la méthode n’accepte pas de paramètre, si bien que l’on spécifie une valeur null pour les paramètres, comme cela est indiqué en gras :

private void tracerButton_Click(objet sender, RoutedEventArgs e) { ...

Page 43: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Action actionFaireFonctionerTracerButton = new Action(faireFonctionnerTracerButton); actionFaireFonctionerTracerButton.BeginInvoke(null, null); }

Le code va se compiler, mais si vous tentez de l’exécuter, il ne fonctionnera pas correctement quand vous cliquerez sur le bouton Tracer graphique. Cela est dû au fait que plusieurs commandes de la méthode faireFonctionnerTracerButton tentent d’accéder à des éléments de l’interface utilisateur, et cette méthode ne s’exécute pas sur le thread d’interface utilisateur. On a déjà rencontré ce problème au chapitre 23, et on a étudié la solution (utilisation de l’objet Dispatcher pour le thread d’interface utilisateur afin d’accéder aux éléments de l’interface utilisateur). Les étapes suivantes modifient ces instructions afin d’utiliser l’objet Dispatcher pour accéder aux éléments de l’interface utilisateur à partir du bon thread.

6. Ajoutez l’instruction using suivante à la liste au sommet du fichier :

using System.Windows.Threading;

L’énumération DispatcherPriority réside dans cet espace de noms. Vous allez utiliser cette énumération quand vous planifiez le code pour qu’il s’exécute sur le thread d’interface utilisateur grâce à l’objet Dispatcher.

7. Au début de la méthode faireFonctionnerTracerButton, examinez l’instruction qui initialise la variable octetsParPixel :

private void faireFonctionnerTracerButton() { int octetsParPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8; ... }

Cette instruction référence l’objet graphBitmap qui appartient au thread d’interface utilisateur. Vous pouvez accéder à cet objet seulement à partir du code qui s’exécute sur le thread d’interface utilisateur. Changez cette instruction pour initialiser la variable octetsParPixel à zéro, et ajoutez une commande pour appeler la méthode Invoke de l’objet Dispatcher, comme cela est indiqué en gras ici :

private void faireFonctionnerTracerButton() { int octetsParPixel = 0; tracerButton.Dispatcher.Invoke(new Action(() =>

Page 44: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

{ octetsParPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8; }), DispatcherPriority.ApplicationIdle); ... }

Si vous vous rappelez le chapitre 23, vous pouvez accéder à l’objet Dispatcher via la propriété Dispatcher de n’importe quel élément de l’interface utilisateur. Ce code utilise le bouton tracerButton. La méthode Invoke attend un délégué et éventuellement une priorité de dispatcher. Dans ce cas, le délégué référence une expression lambda. Le code de cette expression s’exécute sur le thread d’interface utilisateur. Le paramètre DispatcherPriority indique que cette instruction ne doit s’exécuter que lorsque l’application est au repos et il n’y a rien d’autre de plus important dans l’interface utilisateur (comme un clic sur un bouton, la saisie d’un texte, ou le déplacement d’une fenêtre).

8. Examinez les trois dernières instructions de la méthode faireFonctionnerTracerButton méthode :

private void faireFonctionnerTracerButton() { ... duree.Content = string.Format("Duration (ms): {0}", montre.ElapsedMilliseconds); graphBitmap.WritePixels(new Int32Rect(0, 0, largeurPixel, hauteurPixel), donnees, stride, 0); graphImage.Source = graphBitmap; }

Ces instructions référencent les objets duree, graphBitmap et graphImage, qui font tous partie de l’interface utilisateur. En conséquence, vous devez modifier ces instructions pour qu’elles s’exécutent sur le thread d’interface utilisateur.

9. Modifiez ces instructions et exécutez-les en utilisant la méthode Dispatcher.Invoke, comme cela est indiqué en gras ici :

private void faireFonctionnerTracerButton() { ... tracerButton.Dispatcher.Invoke(new Action(() => { duree.Content = string.Format("Durée (ms) : {0}", montre.ElapsedMilliseconds); graphBitmap.WritePixels(new Int32Rect(0, 0, largeurPixel, hauteurPixel), donnees, stride, 0);

Page 45: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

graphImage.Source = graphBitmap; }), DispatcherPriority.ApplicationIdle); }

Ce code convertit ces instructions en une expression lambda encapsulée dans un délégué Action, puis il invoque ce délégué grâce à l’objet Dispatcher.

10. Dans le menu Déboguer, cliquez sur Démarrer sans débogage pour générer et exécuter l’application.

11. Dans la fenêtre Graph Demo, cliquez sur Tracer graphique et avant que le graphique n’apparaisse, déplacez rapidement la fenêtre sur un autre emplacement de l’écran. Vous devriez constater que la fenêtre répond immédiatement et que vous n’avez pas à attendre l’affichage du graphique.

12. Fermez la fenêtre Graph Demo.

Annulation des tâches et gestion des exceptions Parmi les autres exigences courantes des applications qui accomplissent des opérations longues, on compte la faculté d’arrêter ces opérations si nécessaire. Cependant, vous ne devez pas annuler une tâche comme cela, car cela peut corrompre les données de l’application. Dans ces conditions, la TPL implémente une stratégie d’annulation coopérative. L’annulation coopérative permet à une tâche de sélectionner le moment opportun pendant lequel un traitement peut être arrêté et permet également de défaire tout travail qui a été accompli avant l’annulation si nécessaire.

Mécanisme d’annulation coopérative L’annulation coopérative est basée sur la notion de jeton d’annulation. Un jeton d’annulation est une structure qui représente une demande d’annulation d’une ou plusieurs tâches. La méthode qu’une tâche exécute doit inclure un paramètre System.Threading.CancellationToken. Une application qui veut annuler une tâche définit la propriété Boolean IsCancellationRequested de ce paramètre à true. La méthode qui exécute la tâche peut interroger cette propriété à différents moments au cours de son traitement. Si cette propriété est initialisée à set à un certain moment, elle sait que l’application a demandé que la tâche soit annulée. De plus,

Page 46: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

la méthode sait quel travail a été effectué jusque-là, si bien qu’elle peut annuler toutes les modifications si nécessaire avant de se terminer. Sinon, la méthode peut simplement ignorer la demande et continuer son exécution si elle ne souhaite pas annuler la tâche.

Astuce Vous devez interroger le jeton d’annulation d’une tâche fréquemment, mais pas au point d’affecter les performances de la tâche. Si possible, vous devez tendre à vérifier l’annulation au moins toutes les 10 millisecondes, mais pas plus fréquemment que toutes les millisecondes.

Une application obtient un jeton d’annulation CancellationToken en créant un objet System.Threading.CancellationTokenSource et en interrogeant la propriété Token de cet objet. L’application peut ensuite passer cet objet CancellationToken comme paramètre à n’importe quelle méthode démarrée par les tâches créées et exécutées par une application. Si l’application a besoin d’annuler les tâches, elle appelle la méthode Cancel de l’objet CancellationTokenSource. Cette méthode définit la propriété IsCancellationRequested de l’objet CancellationToken passé à toutes les tâches.

L’exemple de code suivant montre comment créer un jeton d’annulation et l’utiliser pour annuler une tâche. La méthode commencerTaches instancie la variable annulationSourceJeton et obtient une référence à l’objet CancellationToken qui est disponible via cette variable. Le code crée et exécute ensuite une tâche qui exécute la méthode faireTravail. Plus tard, le code appelle la méthode Cancel de la source du jeton d’annulation, qui initialise le jeton d’annulation. La méthode faireTravail interroge la propriété IsCancellationRequested du jeton d’annulation. Si la propriété est définie, la méthode se termine ; dans le cas contraire, elle poursuit son exécution.

public class MonApplication { ... // Méthode qui crée et gère une tâche private void commencerTaches() { // Crée la source du jeton d’annulation et obtient un jeton d’annulation CancellationTokenSource annulationSourceJeton = new CancellationTokenSource(); CancellationToken annulationJeton = annulationJeton.Token;

Page 47: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

// Crée une tâche et démarre l’exécution de la méthode faireTravail Task maTache = Task.Factory.StartNew(() => faireTravail(annulationJeton)); ... if (...) { // Annulation de la tâche annulationSourceJeton.Cancel(); } ... } // Méthode exécutée par la tâche private void faireTravail(CancellationToken jeton) { ... // Si l’application a défini le jeton d’annulation, on termine le traitement if (jeton.IsCancellationRequested) { // On range tout et on termine ... return; } // Si la tâche n’a pas été annulée, on continue l’exécution normalement ... } }

Outre le fait qu’elle fournit un bon contrôle sur le traitement de l’annulation, cette approche est évolutive et peut s’adapter à n’importe quel nombre de tâches. Vous pouvez démarrer plusieurs tâches et passer le même objet CancellationToken à chacune d’entre elles. Si vous appelez Cancel sur l’objet CancellationTokenSource, chaque tâche verra que la propriété IsCancellationRequested a été initialisée et peut ainsi réagir en conséquence.

Vous pouvez aussi enregistrer une méthode callback avec le jeton d’annulation en utilisant la méthode Register. Quand une application invoque la méthode Cancel de l’objet correspondant CancellationTokenSource, ce callback s’exécute. Vous ne pouvez cependant pas garantir le moment d’exécution de cette méthode ; ce peut être avant ou après que les tâches ont accompli leur propre traitement d’annulation, ou même au cours de ce traitement.

... annulationJeton.Register(faireTravailSupplementaire); ...

Page 48: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

private void faireTravailSupplementaire() { // Accomplit un traitement supplémentaire d’annulation }

Dans l’exercice suivant, vous allez ajouter une fonctionnalité d’annulation à l’application GraphDemo.

Ajout d’une fonctionnalité d’annulation à l’application GraphDemo

1. Dans Visual Studio 2010, ouvrez la solution GraphDemo, située dans le dossier \Visual C Sharp Etape par étape\Chapitre 27\GraphDemo Annulant Taches de votre dossier Documents.

Il s’agit d’une copie de l’application GraphDemo de l’exercice précédent qui utilise des tâches et des threads pour améliorer la réactivité.

2. Dans l’Explorateur de solutions, dans le projet GraphDemo, faites un double clic sur GraphWindow.xaml pour afficher le formulaire dans la fenêtre Design.

3. À partir de la Boîte à outils, ajoutez un contrôle Button au formulaire sous le label duree. Alignez le bouton horizontalement avec le bouton tracerButton. Dans la fenêtre Propriétés, modifiez la propriété Name du nouveau bouton à annulerButton, et changez la propriété Content à Annuler.

Le formulaire modifié doit ressembler à l’image illustrée ci-dessous :

4. Faites un double clic sur le bouton Annuler afin de créer une méthode de gestionnaire d’événement Click appelée annulerButton_Click.

5. Dans le fichier GraphWindow.xaml.cs, localisez la méthode obtenirDonneesPourGraph. Cette méthode crée les tâches utilisées par l’application et attend qu’elles se terminent. Déplacez la déclaration des

Page 49: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

variables Task au niveau de la classe GraphWindow comme cela est illustré en gras dans le code suivant, puis modifiez la méthode obtenirDonneesPourGraph pour instancier ces variables :

public partial class GraphWindow : Window { ... private Task premiere, deuxieme, troisieme, quatrieme; ... private byte[] obtenirDonneesPourGraph(int tailleDonnees) { byte[] donnees = new byte[tailleDonnees]; premiere = Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0, largeurPixel / 8)); deuxieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 8, largeurPixel / 4)); troisieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 4, largeurPixel * 3 / 8)); quatrieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel * 3 / 8, largeurPixel / 2)); Task.WaitAll(premiere, deuxieme, troisieme, quatrieme); return donnees; } }

6. Ajoutez la déclaration using suivante à la liste au sommet du fichier :

using System.Threading;

Les types utilisés par l’annulation coopérative résident dans cet espace de noms.

7. Ajoutez un membre CancellationTokenSource appelé sourceJeton à la classe GraphWindow, et initialisez-le à null, comme cela est indiqué en gras :

public class GraphWindow : Window { ... private Task premiere, deuxieme, troisieme, quatrieme; private CancellationTokenSource sourceJeton = null; ... }

8. Trouvez la méthode genererDonneesGraph, et ajoutez un paramètre CancellationToken appelé jeton à la définition de la méthode :

private void genererDonneesGraph(byte[] donnees, int partitionDebut, int partitionFin, CancellationToken jeton)

Page 50: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

{ ... }

9. Dans la méthode genererDonneesGraph, au début de la boucle interne for, ajoutez le code suivant illustré en gras pour vérifier si l’annulation a été demandée. Si tel est le cas, quittez la méthode ; dans le cas contraire, continuez à calculer les valeurs et à tracer le graphique.

private void genererDonneesGraph(byte[] donnees, int partitionDebut, int partitionFin, CancellationToken jeton) { int a = largeurPixel / 2; int b = a * a; int c = hauteurPixel / 2; for (int x = partitionDebut; x < partitionFin; x ++) { int s = x * x; double p = Math.Sqrt(b - s); for (double i = -p; i < p; i += 3) { if (jeton.IsCancellationRequested) { return; } double r = Math.Sqrt(s + i * i) / a; double q = (r - 1) * Math.Sin(24 * r); double y = i / 3 + (q * c); tracerXY(donnees, (int)(-x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); tracerXY(donnees, (int)(x + (largeurPixel / 2)), (int)(y + (hauteurPixel / 2))); } } }

10. Dans la méthode obtenirDonneesPourGraph, ajoutez les instructions suivantes illustrées en gras qui instancient la variable sourceJeton et récupèrent l’objet CancellationToken dans une variable appelée jeton :

private byte[] obtenirDonneesPourGraph(int tailleDonnees) { byte[] donnees = new byte[tailleDonnees]; sourceJeton = new CancellationTokenSource(); CancellationToken jeton = sourceJeton.Token; ... }

Page 51: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

11. Modifiez les instructions qui créent et exécutent les quatre tâches, et passent la variable jeton comme dernier paramètre à la méthode genererDonneesGraph :

premiere = Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0, largeurPixel / 8, jeton)); deuxieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 8, largeurPixel / 4, jeton)); troisieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 4, largeurPixel * 3 / 8, jeton)); quatrieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel * 3 / 8, largeurPixel / 2, jeton));

12. Dans la méthode annulerButton_Click, ajoutez le code indiqué ici en gras :

private void annulerButton_Click(objet sender, RoutedEventArgs e) { if (sourceJeton != null) { sourceJeton.Cancel(); } }

Ce code vérifie que la variable sourceJeton a été instanciée ; si c’est le cas, le code invoque la méthode Cancel sur cette variable.

13. Dans le menu Déboguer, cliquez sur Démarrer sans débogage pour créer et exécuter l’application.

14. Dans la fenêtre GraphDemo, cliquez sur le bouton Tracer graphique, et vérifier que le graphique apparaît comme auparavant.

15. Cliquez à nouveau sur Tracer graphique, puis cliquez rapidement sur le bouton Annuler.

Si vous êtes rapide et cliquez sur le bouton Annuler avant que les données du graphique soient générées, cette action provoque l’arrêt des méthodes exécutées par les tâches. Les données ne sont pas complètes si bien que le graphique apparaît avec des trous (voir la figure suivante ; la taille des trous dépend de votre rapidité à cliquer sur le bouton Annuler).

Page 52: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

16. Fermez la fenêtre GraphDemo et retournez à Visual Studio.

Vous pouvez déterminer si une tâche s’est terminée ou a été annulée en examinant la propriété Status de l’objet Task. La propriété Status contient une valeur extraite de l’énumération System.Threading.Tasks.TaskStatus. La liste suivante décrit certaines de ces valeurs d’état que vous risquez de rencontrer :

■ Created C’est l’état initial d’une tâche. Elle a été créée, mais n’a pas encore été planifiée pour s’exécuter.

■ WaitingToRun La tâche a été planifiée, mais elle n’a pas encore commencé à s’exécuter.

■ Running La tâche est actuellement en cours d’exécution dans un thread.

■ RanToCompletion La tâche s’est terminée avec succès sans exception non gérée.

■ Canceled La tâche a été annulée avant de démarrer son exécution, ou elle a accusé réception d’une annulation et s’est terminée sans lever d’exception.

■ Faulted La tâche s’est terminée à cause d’une exception.

Dans l’exercice suivant, vous allez tenter d’afficher l’état de chaque tâche de telle sorte que vous puissiez voir quand elles se sont terminées ou bien quand elles ont été annulées.

Annulation d’une boucle Parallel For ou ForEach Les méthodes Parallel.For et Parallel.ForEach ne fournissent pas un accès direct aux objets Task qui ont été créés. En effet, vous ne savez même pas combien de tâches sont exécutées (le .NET Framework utilise sa propre heuristique pour déterminer le nombre optimal à utiliser en se basant sur les

Page 53: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

ressources disponibles et la charge de travail en cours de l’ordinateur.

Si vous voulez arrêter la méthode Parallel.For ou Parallel.ForEach plus tôt, vous devez d’abord utiliser un objet ParallelLoopState. La méthode que l’on spécifie comme corps de la boucle doit inclure un paramètre supplémentaire ParallelLoopState. La TPL crée un objet ParallelLoopState et le passe en paramètre à la méthode. La TPL utilise cet objet pour stocker des informations sur chaque invocation de méthode. La méthode peut appeler la méthode Stop de cet objet pour indiquer que la TPL ne doit pas tenter d’accomplir des itérations au-delà de celles qui ont déjà démarré et qui sont terminées. L’exemple suivant montre la méthode Parallel.For qui appelle la méthode doLoopWork à chaque itération. La méthode doLoopWork examine la variable d’itération ; si elle est supérieure à 600, la méthode appelle la méthode Stop du paramètre ParallelLoopState. Cela provoque l’arrêt des itérations ultérieures dans la boucle de la méthode Parallel.For (les itérations en cours d’exécution peuvent continuer jusqu’à leur achèvement).

Note Rappelez-vous que les itérations d’une boucle Parallel.For ne s’exécutent pas dans un ordre spécifique. En conséquence, l’annulation de la boucle quand la variable d’itération a atteint la valeur 600 ne garantit pas que les 599 itérations précédentes aient déjà été exécutées. De la même manière, certaines itérations avec des valeurs supérieures à 600 peuvent déjà être terminées.

Parallel.For(0, 1000, doLoopWork); ... private void doLoopWork(int i, ParallelLoopState p) { ... if (i > 600) { p.Stop(); } }

Affichage de l’état de chaque tâche

1. Dans Visual Studio, dans la fenêtre Code, trouvez la méthode obtenirDonneesPourGraph.

Page 54: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

2. Ajoutez le code suivant illustré en gras à cette méthode. Ces instructions génèrent une chaîne qui contient l’état de chaque tâche après la fin de leur exécution, et elles affichent une boîte de message contenant cette chaîne.

private byte[] obtenirDonneesPourGraph(int tailleDonnees) { ... Task.WaitAll(premiere, deuxieme, troisieme, quatrieme); String message = String.Format("Le Status des tâches est {0}, {1}, {2}, {3}", premiere.Status, deuxieme.Status, troisieme.Status, quatrieme.Status); MessageBox.Show(message); return donnees; }

3. Dans le menu Déboguer, cliquez sur Démarrer sans débogage.

4. Dans la fenêtre GraphDemo, cliquez sur le bouton Tracer graphique sans cliquer sur le bouton Annuler. Vérifiez que la boîte de message suivante apparaît, ce qui indique que l’état des tâches est égal à RanToCompletion (quatre fois), puis cliquez sur OK. Notez que le graphique apparaît seulement après que vous avez cliqué sur OK.

5. Dans la fenêtre GraphDemo, cliquez à nouveau sur le bouton Tracer graphique puis cliquez rapidement sur le bouton Annuler.

Il est assez étonnant de constater que la boîte de message qui apparaît affiche toujours l’état de chaque tâche à RanToCompletion, même si le graphique apparaît avec des trous. Cela est dû au fait que malgré la demande d’annulation de chaque tâche à l’aide du jeton d’annulation, les méthodes qu’elles exécutaient se sont simplement arrêtées. Le runtime du .NET Framework ne sait pas si les tâches ont été annulées ou si elles ont été autorisées à terminer leur travail et ont ignoré les demandes d’annulation.

6. Fermez la fenêtre GraphDemo et retournez à Visual Studio.

Page 55: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Dans ces conditions, comment indiquer qu’une tâche a été annulée et n’a pas été autorisée à terminer son travail ? La réponse se trouve dans l’objet CancellationToken passé comme paramètre à la méthode que la tâche exécute. La classe CancellationToken fournit une méthode appelée ThrowIfCancellationRequested. Cette méthode teste la propriété IsCancellationRequested d’un jeton d’annulation ; si elle est égale à true, la méthode lève une exception OperationCanceledException et annule la méthode que la tâche exécute.

L’application qui a démarré le thread doit être préparée à intercepter et gérer cette exception, mais cela introduit une autre question. Si une tâche se termine en levant une exception, elle retourne en fait à l’état Faulted. Cela est vrai, même si l’exception est une exception OperationCanceledException. Une tâche entre dans l’état Canceled seulement si elle est annulée sans lever une exception. Dans ces conditions, comment une tâche peut-elle lever une exception OperationCanceledException sans que cela soit traité comme une exception ?

La réponse se trouve dans la tâche elle-même. Pour qu’une tâche reconnaisse qu’une OperationCanceledException est le résultat de l’annulation d’une tâche d’une manière contrôlée, et pas simplement une exception provoquée par d’autres circonstances, elle doit savoir que l’opération a bien été annulée. Elle ne peut faire cela que si elle peut examiner le jeton d’annulation. Vous avez passé ce jeton en paramètre à la méthode exécutée par la tâche, mais la tâche n’examine en fait aucun de ces paramètres (elle considère que c’est à la méthode de le faire et ne se sent pas concernée). Au lieu de cela, on spécifie le jeton d’annulation quand on crée la tâche, soit comme paramètre du constructeur Task, soit comme paramètre de la méthode StartNew de l’objet TaskFactory que l’on utilise pour créer et exécuter les tâches. Le code suivant illustre un exemple basé sur l’application GraphDemo. Notez la manière dont le paramètre jeton est passé à la méthode genererDonneesGraph (comme auparavant), mais aussi en tant que paramètre séparé à la méthode StartNew :

Task premiere = null; sourceJeton = new CancellationTokenSource(); CancellationToken jeton = sourceJeton.Token; ... premiere = Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0, largeurPixel / 8, jeton), jeton);

Page 56: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

À présent, quand la méthode exécutée par la tâche lève une exception OperationCanceledException, l’infrastructure derrière la tâche examine le jeton CancellationToken. S’il indique que la tâche a été annulée, l’infrastructure gère l’exception OperationCanceledException, accuse réception de l’annulation et définit l’état de la tâche à Canceled. L’infrastructure lève ensuite une exception TaskCanceledException, que votre application doit être préparée à intercepter. C’est ce que vous allez faire dans le prochain exercice, mais avant de réaliser cela, vous devez en savoir un peu plus sur la manière dont les tâches lèvent les exceptions et apprendre à les gérer.

Gestion des exceptions de tâches grâce à la classe AggregateException Vous avez vu tout au long de cet ouvrage que la gestion des exceptions est un élément important de toute application commerciale. Les constructions de gestion des exceptions que vous avez rencontrées jusqu’à présent sont simples à utiliser, et si vous les employez avec précaution, il s’agit tout simplement d’intercepter une exception et de déterminer quelle partie de code doit être exécutée. Cependant, quand on commence à diviser le travail en plusieurs tâches concurrentes, le suivi et la gestion des exceptions devient un problème plus complexe. La difficulté provient que les différentes tâches peuvent chacune générer leurs propres exceptions, et vous avez besoin de trouver un moyen pour intercepter et gérer plusieurs exceptions qui peuvent être levées en même temps. C’est à ce moment-là que la classe AggregateException intervient.

AggregateException permet d’encapsuler une collection d’exceptions. Chacune des exceptions de la collection peut être levée par différentes tâches. Dans votre application, vous pouvez intercepter l’exception AggregateException et parcourir ensuite cette collection pour accomplir tout traitement nécessaire. Pour vous faciliter la tâche, la classe AggregateException fournit la méthode Handle. Cette méthode accepte un délégué Func<Exception, bool> qui référence une méthode. La méthode référencée prend un objet Exception comme paramètre et retourne une valeur Boolean. Quand on appelle Handle, la méthode référencée s’exécute pour chaque exception de la collection de l’objet AggregateException. La méthode référencée peut examiner l’exception et entreprendre l’action appropriée. Si la méthode référencée gère l’exception, elle doit retourner true. Si ce n’est pas le cas, elle doit retourner false. Quand la méthode Handle se termine, toutes les

Page 57: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

exceptions non gérées sont regroupées dans une nouvelle AggregateException et cette exception est levée ; un gestionnaire d’exception externe ultérieur peut ensuite intercepter cette exception et la traiter.

Dans l’exercice suivant, vous allez voir comment intercepter une exception AggregateException et l’utiliser pour gérer l’exception TaskCanceledException levée quand une tâche est annulée.

Accusé de réception d’annulation et gestion de l’exception AggregateException

1. Dans Visual Studio, affichez le fichier GraphWindow.xaml dans la fenêtre Design.

2. À partir de la Boîte à outils, ajoutez un contrôle Label au formulaire, sous le bouton annulerButton. Alignez le bord gauche du contrôle Label avec le bord gauche du bouton annulerButton.

3. En utilisant la Fenêtre de propriétés, modifiez la propriété Name du contrôle Label en status, et supprimez la valeur de la propriété Content.

4. Retournez à la fenêtre Code qui affiche le fichier GraphWindow.xaml.cs, et ajoutez la méthode suivante sous la méthode obtenirDonneesPourGraph :

private bool gererException(Exception e) { if (e is TaskCanceledException) { tracerButton.Dispatcher.Invoke(new Action(() => { status.Content = "Tâches annulées"; }), DispatcherPriority.ApplicationIdle); return true; } else { return false; } }

Cette méthode examine l’objet Exception passé comme paramètre ; si c’est un objet TaskCanceledException, la méthode affiche le texte « Tâches annulées » dans l’étiquette status sur le formulaire et retourne true pour indiquer qu’elle a géré l’exception ; dans le cas contraire, elle retourne false.

Page 58: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

5. Dans la méthode obtenirDonneesPourGraph, modifiez les instructions qui créent et exécutent les tâches et spécifiez l’objet CancellationToken comme deuxième paramètre de la méthode StartNew, comme cela est indiqué en gras dans le code suivant :

private byte[] obtenirDonneesPourGraph(int tailleDonnees) { byte[] donnees = new byte[tailleDonnees]; sourceJeton = new CancellationTokenSource(); CancellationToken jeton = sourceJeton.Token; ... premiere = Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0, largeurPixel / 8, jeton), jeton); deuxieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 8, largeurPixel / 4, jeton), jeton); troisieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 4, largeurPixel * 3 / 8, jeton), jeton); quatrieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel * 3 / 8, largeurPixel / 2, jeton), jeton); Task.WaitAll(premiere, deuxieme, troisieme, quatrieme); ... }

6. Ajoutez un bloc try autour des commandes qui créent et exécutent les tâches, et attendent qu’elles se terminent. Si l’achèvement est réussi, affichez le texte « Tâches terminées » dans l’étiquette status sur le formulaire grâce à la méthode Dispatcher.Invoke. Ajoutez un bloc catch qui gère l’exception AggregateException. Dans ce gestionnaire d’exception, appelez la méthode Handle de l’objet AggregateException et passez une référence à la méthode gererException. Le code ci-dessous en gras attire votre attention sur les modifications que vous devez faire :

private byte[] obtenirDonneesPourGraph(int tailleDonnees) { byte[] donnees = new byte[tailleDonnees]; sourceJeton = new CancellationTokenSource(); CancellationToken jeton = sourceJeton.Token; try { premiere = Task.Factory.StartNew(() => genererDonneesGraph(donnees, 0, largeurPixel / 8, jeton), jeton); deuxieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 8, largeurPixel / 4, jeton), jeton); troisieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel / 4, largeurPixel * 3 / 8, jeton), jeton);

Page 59: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

quatrieme = Task.Factory.StartNew(() => genererDonneesGraph(donnees, largeurPixel * 3 / 8, largeurPixel / 2, jeton), jeton); Task.WaitAll(premiere, deuxieme, troisieme, quatrieme); tracerButton.Dispatcher.Invoke(new Action(() => { status.Content = "Tâches terminées"; }), DispatcherPriority.ApplicationIdle); } catch (AggregateException ae) { ae.Handle(gererException); } String message = String.Format("Le Status des tâches est {0}, {1}, {2}, {3}", premiere.Status, deuxieme.Status, troisieme.Status, quatrieme.Status); MessageBox.Show(message); return donnees; }

7. Dans la méthode generateDataForGraph, remplacez l’instruction if qui examine la propriété IsCancellationProperty de l’objet CancellationToken par le code qui appelle la méthode ThrowIfCancellationRequested, comme cela est indiqué ici en gras :

private void generateDataForGraph(byte[] donnees, int partitionDebut, int partitionFin, CancellationToken jeton) { ... for (int x = partitionDebut; x < partitionFin; x++); { ... for (double i = -p; I < p; i += 3) { jeton.ThrowIfCancellationRequested(); ... } } ... }

8. Dans le menu Déboguer, cliquez sur Démarrer sans débogage.

9. Dans la fenêtre Graph Demo, cliquez sur le bouton Tracer graphique et vérifiez que l’état de chaque tâche est affiché en tant que RanToCompletion, que le graphique est généré et que l’étiquette status affiche le message

Page 60: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

« Tâches terminées ».

10. Cliquez à nouveau sur le bouton Tracer graphique, puis cliquez rapidement sur le bouton Annuler. Si vous êtes rapide, l’état d’une ou plusieurs tâches doit être affiché en tant que Canceled, l’étiquette status doit afficher le texte « Tâches annulées », et le graphique doit être affiché avec des trous. Si vous n’êtes pas assez rapide, répétez cette étape pour obtenir ce résultat.

11. Fermez la fenêtre Graph Demo et retournez à Visual Studio.

Utilisation de continuations avec des tâches annulées ou en échec Si vous devez accomplir un travail supplémentaire quand une tâche est annulée ou lève une exception non gérée, rappelez-vous que vous pouvez utiliser la méthode ContinueWith avec la valeur appropriée TaskContinuationOptions. Par exemple, le code suivant crée une tâche qui exécute la méthode faireTravail. Si la tâche est annulée, la méthode ContinueWith spécifie qu’une autre tâche doit être créée et exécuter la méthode doCancellationWork. Cette méthode peut accomplir une simple routine de connexion ou bien une remise en ordre. Si la tâche n’est pas annulée, la continuation ne s’exécute pas.

Task tache = new Task(faireTravail); tache.ContinueWith(doCancellationWork, TaskContinuationOptions.OnlyOnCanceled); tache.Start(); ... private void faireTravail() { // La tâche exécute ce code quand elle est démarrée ... } ... private void doCancellationWork(Task tache) { // La tâche exécute ce code quand faireTravail se termine ... }

De la même manière, vous pouvez indiquer la valeur TaskContinuationOptions.OnlyOnFaulted pour spécifier une continuation qui s’exécute si la méthode originale exécutée par la tâche lève une exception non gérée.

Page 61: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Dans ce chapitre, vous avez appris pourquoi il est important d’écrire des applications qui peuvent évoluer pour des machines dotées de plusieurs processeurs. Vous avez vu comment utiliser la Bibliothèque parallèle de tâches pour exécuter des opérations en parallèle, et comment synchroniser des opérations concurrentes et attendre qu’elles se terminent. Vous avez aussi appris à utiliser la classe Parallel pour paralléliser certaines constructions courantes de programmation, et vous avez vu quand la parallélisation du code n’est pas appropriée. Vous avez utilisé les tâches et les threads ensemble dans une interface graphique utilisateur afin d’améliorer la réactivité et le débit. Enfin, vous avez vu comment annuler des tâches de manière propre et contrôlée.

Aide-mémoire du chapitre 27 Pour Accomplissez

Créer une tâche et l’exécuter Utilisez la méthode StartNew d’un objet TaskFactory pour créer et exécuter la tâche en une seule étape :

Task tache = taskFactory.StartNew(faireTravail()); ... private void faireTravail() { // La tâche exécute ce code quand elle est démarrée ... }

Ou créez un nouvel objet Task qui référence une méthode à exécuter et appelez la méthode Start :

Task tache = new Task(faireTravail); tache.Start();

Attendre qu’une tâche se termine Appelez la méthode Wait de l’objet Task :

Task tache = ...; ... tache.Wait();

Attendre que plusieurs tâches se terminent

Appelez la méthode statique WaitAll de la classe Task, et spécifiez les tâches à attendre :

Task tache1 = ...;

Page 62: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Chapitre 27 – Introduction à la Bibliothèque parallèle de tâches

Pour Accomplissez

Task tache2 = ...; Task tache3 = ...; Task tache4 = ...; ... Task.WaitAll(tache1, tache2, tache3, tache4);

Spécifier une méthode à exécuter dans une nouvelle tâche quand une tâche est terminée

Appelez la méthode ContinueWith de la tâche, et spécifiez la méthode en tant que continuation :

Task tache = new Task(faireTravail); tache.ContinueWith(faireAutreTravail, TaskContinuationOptions.NotOnFaulted);

Retourner une valeur à partir d’une tâche

Utilisez un objet Task<TResult> pour exécuter une méthode, où le paramètre type T spécifie le type de la valeur de retour de la méthode. Utilisez la propriété Result de la tâche à attendre pour que la tâche se termine et retournez la valeur :

Task<int> tacheCalculerValeur = new Task<int>(() => calculerValeur(...)); tacheCalculerValeur.Start(); // Invoque la méthode calculerValeur ... int donneesCalculees = tacheCalculerValeur.Result; // Bloque jusquà ce que tacheCalculerValeur se termine

Accomplir des itérations de boucles et des séquences d’instructions grâce à des tâches parallèles

Utilisez les méthodes Parallel.For et Parallel.ForEach pour accomplir des itérations de boucles grâce à des tâches :

Parallel.For(0, 100, performLoopProcessing); ... private void performLoopProcessing(int x) { // Accomplit le traitement de la boucle }

Utilisez la méthode Parallel.Invoke pour accomplir les appels de méthodes concurrentes grâce à des tâches séparées :

Parallel.Invoke( faireTravail, faireAutreTravail, doYetMoreWork );

Gérer des exceptions levées par Interceptez l’exception AggregateException. Utilisez la méthode Handle

Page 63: Chapitre27 Visual C# 2010 etape par etape - multi threading WPF C# .net

Partie 6 – Création de solutions professionnelles

© Dunod 2010 – Visual C# 2010 Étape par étape – John Sharp

Pour Accomplissez

une ou plusieurs tâches pour spécifier une méthode qui peut gérer chaque exception de l’objet AggregateException. Si la méthode qui gère l’exception peut la prendre en charge, retournez true ; dans le cas contraire, retournez false :

try { Task tache = Task.Factory.StartNew(...); ... } catch (AggregateException ae) { ae.Handle(new Func<Exception, bool> (gererException)); } ... private bool gererException(Exception e) { if (e is TaskCanceledException) { ... return true; } else { return false; } }

Prendre en charge l’annulation d’une tâche

Implémentez l’annulation coopérative en créant un objet CancellationTokenSource et en utilisant le paramètre CancellationToken dans la méthode exécutée par la tâche. Dans la méthode de la tâche, appelez la méthode ThrowIfCancellationRequested du paramètre CancellationToken pour lever une exception OperationCanceledException et terminez la tâche :

private void genererDonneesGraph(..., CancellationToken jeton) { ... jeton.ThrowIfCancellationRequested(); ... }