Exporter (0) Imprimer
Développer tout

Mise en œuvre d'un processus d'arrière-plan dans Visual Basic .NET

Rockford Lhotka
Magenic Technologies

Télécharger le fichier exemple VBbackground.exe à partir du MSDN Code Center (leave-msdn france Site en anglais). Les commentaires des programmeurs sont en anglais dans les exemples de fichiers téléchargés, mais pour plus de clarté ils ont été traduits dans l'article.

Résumé : Rocky Lhotka décrit et met en œuvre un exemple de structure organisée servant d'intermédiaire entre les threads de travail et la thread de l'interface utilisateur (ou thread UI), qui simplifie l'écriture d'un code de traitement multi-thread et d'une interface utilisateur pour le contrôler. Cette structure (framework), modifiable en fonction des besoins de votre application, inclut un exemple de code téléchargeable.

Le multithreading permet à une application d'exécuter plusieurs tâches à la fois. Il est ainsi possible d'avoir une thread qui exécute l'interface utilisateur (UI) et une autre qui effectue des calculs ou des opérations d'arrière-plan à un rythme intensif. Microsoft® Visual Basic® .NET prend en charge le multithreading, ce qui va nous permettre d'en exploiter les puissantes fonctionnalités.

Malheureusement, le multithreading n'a pas que des avantages. Chaque fois qu'une application utilise plusieurs threads, ces dernières risquent d'intervenir toutes en même temps sur les mêmes données ou les mêmes ressources. Dans ce cas, la situation devient complexe et difficile à démêler.

Par ailleurs, le code multithread semble fonctionner correctement lors du développement initial, mais au moment de la production, les choses se gâtent en raison de l'interaction de plusieurs threads sur les mêmes données ou ressources. La programmation multithread n'est donc pas sans dangers !

Devant la difficulté de concevoir et de déboguer les applications multithread, Microsoft a créé le concept de cloisonnement de threads unique (STA, Single-Thread Apartment) dans le modèle COM. Jusqu'alors, le code Visual Basic 6 s'exécutait en mode STA, de sorte que notre code n'avait toujours qu'une seule thread à prendre en charge. Nous évitions ainsi tout problème de partage des données ou des ressources ; en revanche, nous ne pouvions profiter des avantages du multithreading sans recourir à des mesures extrêmes.

Dans .NET, il n'existe rien de semblable au modèle STA. Tout le code .NET s'exécute dans un AppDomain, ou domaine d'applications, qui reconnaît le multithreading. Par conséquent, Visual Basic .NET peut aussi s'exécuter dans un AppDomain et tirer parti du multithreading. Dans ce cas, nous devons écrire un code qui évite les conflits entre threads.

Le plus simple est de faire en sorte qu'elles n'interagissent jamais sur les mêmes données ou les mêmes ressources. Bien que l'opération soit difficile, il faut veiller à ce que les applications multithread ne partagent pas, ou très peu, des ressources ou des données.

Non seulement le code et le débogage en seront simplifiés, mais les performances en seront également améliorées. Pour résoudre les conflits entre threads, nous devons recourir à des techniques de synchronisation qui entraînent souvent le blocage des threads ou leur interruption momentanée jusqu'à ce qu'une autre thread termine sa tâche. Lorsqu'une thread est bloquée, elle est inactive et, naturellement, les performances diminuent.

Bouton Annuler et affichage de l'état

Plusieurs raisons justifient l'utilisation du multithreading dans une application, mais la plus courante est la nécessité d'exécuter une tâche de longue durée pendant laquelle nous souhaitons que l'interface utilisateur reste disponible.

Au minimum, un bouton Annuler doit être accessible afin que les utilisateurs puissent demander l'interruption d'une tâche longue à s'exécuter.

Dans Visual Basic 6, nous utilisions pour ce faire des contrôles DoEvents, des minuteurs et autres mécanismes divers. Dans Visual Basic .NET, les choses sont plus simples grâce au multithreading qui, si nous nous montrons prudents, évite d'accroître la complexité du code ou du débogage.

Lorsque nous plaçons un bouton Annuler dans un environnement multithread, n'oublions pas qu'il a simplement pour but de demander l'annulation de la tâche d'arrière-plan. C'est à elle qu'il appartient de s'interrompre au moment adéquat.

Si notre bouton Annuler arrêtait directement le processus d'arrière-plan, nous risquerions de l'arrêter au milieu d'une opération critique ou avant qu'il n'ait eu le temps de fermer des ressources importantes, telles que des gestionnaires de fichiers ou des connexions à des bases de données. Les conséquences pourraient s'avérer désastreuses, entraînant des blocages, un comportement instable de l'application ou une panne définitive.

En réalité, le bouton Annuler doit toujours demander l'arrêt de la tâche en arrière-plan. Celle-ci peut vérifier à certains moments ou endroits appropriés si une opération d'annulation a été demandée. Si c'est le cas, la thread d'arrière-plan libère les ressources, met un terme aux activités critiques et s'arrête de façon ordonnée.

Il est donc important qu'il y ait une demande d'annulation, mais il est tout aussi essentiel que l'interface utilisateur affiche une information sur l'état du processus d'arrière-plan. Cette information peut être un message, un pourcentage d'exécution, ou les deux.

La difficulté majeure rencontrée lorsque nous implémentons un bouton Annuler ou que nous voulons afficher une information d'état dans Visual Basic .NET est que la bibliothèque des Windows Forms n'est pas sécurisée vis-à-vis des threads. Ce qui signifie que seule la thread créatrice d'un formulaire peut interagir avec celui-ci ou avec ses contrôles. Aucune autre thread ne peut intervenir en toute sécurité sur le formulaire ou ses contrôles.

Malheureusement, rien n'est fait pour nous empêcher d'écrire du code dans lequel plusieurs threads interagissent avec un formulaire. Rien, si ce n'est les résultats imprévisibles et les pannes potentielles de l'application qui risquent de se produire au moment de l'exécution...

Par conséquent, lorsque nous écrivons du code, nous devons faire preuve d'une grande prudence afin d'être certain que seule notre thread UI interagit avec l'interface elle-même. Pour nous aider, nous pouvons constituer une structure simple destinée à gérer l'interaction entre notre thread de travail en arrière-plan et la thread UI. Si nous procédons correctement, nous réussirons à utiliser le multithreading de façon relativement transparente pour le code UI et pour le code de la tâche à exécution longue.

Threading et objets

Le plus simple, pour créer un processus d'arrière-plan qui s'exécute sur sa propre thread avec ses propres données, consiste à créer un objet spécialement pour ce processus. Cette méthode a l'avantage de simplifier considérablement la création d'applications multithread.

Si la thread d'arrière-plan s'exécute avec son propre objet, elle peut utiliser des variables d'instance de l'objet (variables déclarées dans la classe) sans chercher à savoir si d'autres threads les utilisent. Par exemple, prenons la classe suivante :

Public Class Worker
 Private mInner As Integer
 Private mOuter As Integer

 Public Sub New(ByVal InnerSize As Integer, ByVal OuterSize As Integer)
 mInner = InnerSize
 mOuter = OuterSize
 End Sub

 Public Sub Work()
 Dim innerIndex As Integer
 Dim outerIndex As Integer

 Dim value As Double

 For outerIndex = 0 To mOuter
 For innerIndex = 0 To mInner
 ' Exécution de certains calculs
 value = Math.Sqrt(CDbl(innerIndex - outerIndex))
 Next
 Next
 End Sub

End Class

Cette classe est conçue pour s'exécuter dans une thread d'arrière-plan et peut être lancée au moyen d'un code tel que celui-ci :

Dim myWorker As New Worker(10000000, 10)
Dim backThread As New Thread(AddressOf myWorker.Work)
backThread.Start()

La classe Worker possède des variables d'instance qui contiennent ses données. Ces variables, mInner et mOuter, sont utilisables en toute sécurité par la thread d'arrière-plan sans que d'autres threads y accèdent en même temps.

Elle possède aussi une méthode constructor permettant d'initialiser l'objet avec des données de démarrage. Avant de lancer la thread d'arrière-plan, le code principal de l'application crée une instance de cet objet et l'initialise avec les données sur lesquelles la thread va agir.

L'adresse de la méthode Work de l'objet est transmise à la thread d'arrière-plan, qui est ensuite lancée. Cette thread va dès lors exécuter le code à l'intérieur de l'objet, en utilisant les données spécifiques de cet objet.

Du fait que l'objet est autonome, nous pouvons créer plusieurs objets qui s'exécutent sur leur propre thread et sont isolés les uns des autres.

Toutefois, cette implémentation n'est pas idéale. En effet, rien ici ne permet à l'interface de connaître l'état du processus d'arrière-plan. En outre, nous n'avons pas non plus prévu de procédé qui permettrait à l'interface de demander la fin du processus d'arrière-plan.

Pour remplir ces deux conditions, la thread d'arrière-plan doit pouvoir dans une certaine mesure interagir avec la thread UI. Or, c'est précisément cette interaction qui pose problème ; aussi devons-nous chercher à l'englober et en quelque sorte à l'isoler dans une classe afin que ni l'interface utilisateur, ni le code de travail, n'aient à s'en préoccuper.

Architecture

Nous allons créer une architecture qui « protège » le code de travail et celui de l'interface de toute interaction avec le mécanisme de threading. En fait, nous pouvons concevoir une structure qui implémente le code complexe de telle sorte qu'il nous aide à gérer ou contrôler les threads d'arrière-plan et leur interaction avec l'interface utilisateur.

Examinons d'abord l'architecture, puis nous passerons à la conception et à l'implémentation du code. Les données téléchargeables avec cet article comprennent ce code ainsi qu'un exemple d'application qui en illustre le fonctionnement.

Généralement, une application démarre avec une thread qui ouvre l'interface utilisateur. Pour plus de simplicité, appelons-la thread UI. Dans bon nombre d'applications, cette thread est seule et elle gère l'interface ainsi que toutes les opérations de traitement.

Dans notre cas toutefois, nous allons créer une thread de travail qui va exécuter certaines opérations en arrière-plan ; de son côté, la thread UI ne s'occupera que de l'interface utilisateur afin de rester disponible pendant l'exécution des tâches par la thread de travail.

Entre la thread UI et la thread de travail, nous allons insérer une couche logique qui servira d'intermédiaire entre le code UI et le code de travail. Ce code est un composant Controller qui gère et contrôle la thread de travail et son interaction avec l'interface utilisateur.

Figure 1. Thread UI, Controller et thread de travail (Worker)

Figure 1. Thread UI, Controller et thread de travail (Worker)

Le Controller va contenir l'ensemble du code nécessaire pour démarrer la thread de travail, transmettre les messages d'état de la thread de travail à la thread UI et envoyer les demandes d'annulation de la thread UI à la thread de travail. Aucune interaction directe n'aura lieu entre le code UI et le code de travail puisque ceux-ci passeront toujours par le contrôleur.

Toutefois, il existe une exception à ce principe, car avant et après activation de la thread de travail, le code UI peut interagir avec l'objet Worker. Avant que la thread de travail ne démarre, l'interface utilisateur peut créer et initialiser l'objet Worker. Une fois que la thread de travail a terminé, l'interface utilisateur peut récupérer des valeurs de l'objet Worker. Du point de vue de l'interface, il en résulte le flux d'événements suivant :

  1. L'objet Worker est créé.
  2. L'objet Worker est initialisé.
  3. Le Controller est appelé pour démarrer la thread de travail.
    1. L'objet Worker peut envoyer des informations d'état à l'interface utilisateur via le Controller.
    2. L'interface utilisateur peut envoyer une demande d'annulation à l'objet Worker via le Controller.
  4. Via le Controller, l'objet Worker avertit l'interface utilisateur qu'il a terminé.
  5. Le cas échéant, des valeurs sont directement récupérées à partir de l'objet Worker.

Hormis le fait que le code UI ne peut pas interagir directement avec l'objet Worker pendant que la thread de travail est active, aucun codage particulier n'est nécessaire pour l'interface utilisateur. Celle-ci reste active et disponible même pendant le traitement d'arrière-plan.

Du point de vue de l'objet Worker, le flux des événements est le suivant :

  1. Le code UI crée l'objet Worker.
  2. Le code UI initialise l'objet Worker avec les données nécessaires.
  3. Le Controller crée une thread d'arrière-plan et appelle la méthode de l'objet Worker.
    1. L'objet Worker exécute le code de travail.
    2. L'objet Worker transmet des informations d'état au Controller qui, à son tour, les communique à l'interface utilisateur.
    3. Le cas échéant, l'objet Worker détecte une demande d'annulation et, au besoin, s'arrête.
    4. Lorsque l'objet Worker a terminé, il l'indique au Controller afin que l'information soit transmise à l'interface utilisateur.
  4. Une fois que la thread de travail a terminé, l'interface utilisateur peut interagir directement avec l'objet Worker.

Étant donné que le code de travail n'a d'interaction qu'avec le Controller, nous n'avons pas à nous demander s'il va interagir accidentellement avec les composants UI, ce qui déstabiliserait sans aucun doute l'application. En fait, le code de travail compte sur le Controller pour communiquer en toute sécurité avec la thread UI.

En définitive, dans notre code de travail, nous n'avons pas à nous préoccuper des problèmes de threading dès l'instant que nous n'avons affaire qu'aux variables d'instance contenues dans l'objet Worker.

Pour avoir un aperçu des interactions entre les différents composants, notamment avec des threads différentes, vous pouvez recourir à des diagrammes. Microsoft® Visio® permet la création de diagrammes UML (Universal Modeling Language) souvent très utiles.

Voici un diagramme de séquences UML qui illustre le flux des événements entre l'interface utilisateur, l'objet Worker et le Controller. Ici, aucune opération d'annulation n'est demandée. La barre d'activité verticale qui coïncide avec les lignes verticales sous les objets Worker et Controller met en évidence le code qui s'exécute dans la thread de travail. Le reste du code s'exécute entièrement dans la thread UI.

Figure 2. Diagramme de séquences illustrant le flux des processus

Figure 2. Diagramme de séquences illustrant le flux des processus

Une autre façon d'examiner ces données consiste à utiliser un diagramme d'activité UML. Ce type de diagramme s'attache plus aux tâches qu'aux objets, de sorte qu'il affiche la succession d'étapes et le déroulement progressif du processus. Nous voyons très clairement le code UI regroupé dans une thread à gauche, et l'objet Worker dans une autre thread à droite. Avant et après son exécution dans l'autre thread, l'objet Worker est utilisable par l'interface utilisateur pour initialiser des valeurs et récupérer ensuite les résultats.

Figure 3. Diagramme d'activité montrant le flux des processus

Figure 3. Diagramme d'activité montrant le flux des processus

L'utilisation de ce type de diagrammes peut nous aider à mettre en évidence les endroits où, par accident, l'interface utilisateur interagit directement avec l'objet Worker (et inversement) pendant que la thread d'arrière-plan est active. Ce type d'interaction doit être reprogrammée, afin d'éviter les bogues susceptibles de déstabiliser l'application. Dans l'idéal, ces interactions doivent être acheminées via le Controller, dans lequel nous pouvons regrouper l'ensemble du code qui les concerne, de façon à les sécuriser.

La figure suivante illustre la séquence d'événements en cas de demande d'annulation transmise à l'interface utilisateur.

Figure 4. Diagramme de séquences illustrant une demande d'annulation

Figure 4. Diagramme de séquences illustrant une demande d'annulation

Notez que l'interface utilisateur transmet la demande d'annulation au Controller, et qu'il revient à l'objet Worker de vérifier auprès du Controller si une demande d'annulation a eu lieu. Ni l'interface utilisateur ni le Controller n'obligent le code de travail à prendre fin, ce qui permet à ce dernier de s'arrêter de manière ordonnée et en toute sécurité.

Conception de la structure

Pour obtenir le comportement décrit ci-dessus, nous avons besoin d'une classe Controller. Par ailleurs, afin que cette structure soit utilisable dans différents scénarios, nous allons définir des interfaces formelles que notre Controller pourra utiliser en cas d'interaction avec la thread UI (ou client) et la thread de travail.

La définition d'interfaces formelles pour les deux threads, client et de travail, permet d'utiliser le même objet Controller dans plusieurs situations, avec des besoins d'interface et des objets Worker différents.

Le diagramme de classes UML suivant montre la classe Controller ainsi que les interfaces IClient et IWorker. Il montre aussi une interface IController que le code de travail utilisera pour interagir avec l'objet Controller.

Figure 5. Diagramme de classes du contrôleur et des interfaces associées

Figure 5. Diagramme de classes du contrôleur et des interfaces associées

L'interface IClient définit les méthodes qui seront appelées par l'objet Controller pour signaler à l'interface client le début et la fin d'exécution de l'objet Worker et lui transmettre en même temps les messages d'état intermédiaire. Elle inclut aussi une méthode pour signaler l'échec du code de travail.

Bien souvent, il est possible de mettre ces éléments en œuvre sous la forme d'événements déclenchés par l'objet Controller et traités par la thread UI. Cependant, il n'est guère facile de déclencher un événement à partir de la thread de travail de telle sorte qu'il soit correctement géré par la thread UI ; par conséquent, nous allons implémenter un ensemble de méthodes.

En effet, nous pouvons assez facilement obtenir que le code contrôleur exécuté par la thread de travail appelle ces méthodes dans l'interface utilisateur afin qu'elles soient traitées par la thread UI.

De même, l'interface IWorker définit les méthodes qui seront appelées par l'objet Controller afin d'interagir avec le code de travail. La méthode Initialize fournit au code de travail une référence à l'objet Controller et la méthode Start démarre le traitement dans la thread d'arrière-plan.

En raison du mode de fonctionnement du threading, la méthode Start ne peut pas avoir de paramètres. Lorsque nous démarrons une nouvelle thread, nous devons lui transmettre l'adresse d'une méthode qui n'accepte aucun paramètre.

Là encore, notez qu'aucune méthode Cancel ou Stop ne figure dans l'interface IWorker. Ce n'est pas nécessaire, puisque nous ne pouvons pas obliger le code de travail à s'arrêter ; en revanche, le code de travail peut utiliser l'interface IController pour vérifier auprès de l'objet Controller si une annulation a été demandée.

L'interface IController définit les méthodes que le code de travail peut appeler sur l'objet Controller. Elle permet au code de travail de vérifier l'existence d'un indicateur Running, qui est égal à False en cas de demande d'annulation. Elle lui permet aussi d'avertir l'objet Controller que le travail est terminé ou a échoué et de lui communiquer les messages d'état ainsi que le pourcentage d'exécution (un entier entre 0 et 100).

Enfin, il s'agit de définir l'objet Controller lui-même. Celui-ci contient des méthodes que le code UI peut appeler, notamment la méthode Start qui démarre le traitement d'arrière-plan en transmettant à l'objet Controller une référence à l'objet Worker. Il comprend aussi une méthode Cancel permettant de demander une annulation. L'interface utilisateur peut également vérifier la propriété Running afin de déterminer si une annulation a été demandée ou non, ainsi que la propriété Percent pour connaître le pourcentage d'exécution.

La classe Controller inclut une méthode constructor permettant d'accepter IClient comme paramètre, afin que l'interface fournisse à l'objet Controller une référence au formulaire qui va gérer l'affichage des messages provenant de Worker.

Pour mettre en œuvre un ensemble animé de points représentant une activité, nous allons créer un contrôle Windows Forms qui utilise un minuteur (timer) pour modifier les couleurs d'un groupe de contrôles PictureBox.

Implémentation

Nous allons implémenter cette structure dans un projet de Bibliothèque de classes (Class Library) afin qu'elle soit utilisable dans toutes les applications devant exécuter un processus d'arrière-plan.

Ouvrez Visual Studio .NET, puis créez une application Bibliothèque de classes nommée Background. Cette bibliothèque devant contenir un contrôle Windows Forms ainsi qu'un formulaire, nous devons faire référence aux deux fichiers System.Windows.Forms.dll et System.Windows.Drawing.dll, en utilisant la boîte de dialogue Ajouter une référence. De plus, nous pouvons importer ces espaces de noms au niveau du projet en utilisant la boîte de dialogue des propriétés, comme le montre la Figure 6.

Figure 6. Utilisation des propriétés du projet pour importer des espaces de noms au niveau du projet

Figure 6. Utilisation des propriétés du projet pour importer des espaces de noms au niveau du projet

Une fois cette opération effectuée, nous pouvons écrire le code. Commençons par créer les interfaces.

Définition des interfaces

Ajoutez une classe au projet IClient et remplacez le code existant par le suivant :

Public Interface IClient
 Sub Start(ByVal Controller As Controller)
 Sub Display(ByVal Text As String)
 Sub Failed(ByVal e As Exception)
 Sub Completed(ByVal Cancelled As Boolean)
End Interface

Ajoutez ensuite une classe nommée IWorker et remplacez son code par le suivant :

Public Interface IWorker
 Sub Initialize(ByVal Controller As IController)
 Sub Start()
End Interface

Pour finir, ajoutez une classe nommée IController dont le code est le suivant :

Public Interface IController
 ReadOnly Property Running() As Boolean
 Sub Display(ByVal Text As String)
 Sub SetPercent(ByVal Percent As Integer)
 Sub Failed(ByVal e As Exception)
 Sub Completed(ByVal Cancelled As Boolean)
End Interface

À ce stade, nous avons défini toutes les interfaces décrites dans le diagramme de classes évoqué plus haut. Nous pouvons à présent implémenter la classe Controller.

Classe Controller

La classe Controller constitue le cœur de notre structure. Elle va contenir le code permettant de démarrer la thread de travail et de faire le lien entre les threads UI et de travail, jusqu'à ce que la thread de travail soit terminée.

Pour ajouter cette classe appelée Controller, nous allons d'abord ajouter un composant Imports et déclarer des variables :

Imports System.Threading

Public Class Controller
 Implements IController

 Private mWorker As IWorker
 Private mClient As Form
 Private mRunning As Boolean
 Private mPercent As Integer

Nous devons également déclarer des délégués. Un délégué est un pointeur formel sur une méthode et un délégué de méthode doit avoir la même signature de méthode (types de paramètres, etc.) que la méthode elle-même.

Les délégués ont des utilisations multiples. Dans notre cas, ils sont importants car ils permettent à une thread d'appeler une méthode sur un formulaire afin qu'elle s'exécute dans la thread UI du formulaire. Nous avons aussi besoin d'un délégué pour chacune des trois méthodes que nous appellerons sur le formulaire défini par IClient :

' cette signature de délégué correspond à celle de 
 ' IClient.Completed et permet d'appeler en toute
 ' sécurité cette méthode sur la thread UI 
 Private Delegate Sub CompletedDelegate(ByVal Cancelled As Boolean)

 ' cette signature de délégué correspond à celle de 
 ' IClient.Display et permet d'appeler en toute
 ' sécurité cette méthode sur la thread UI 
 Private Delegate Sub DisplayDelegate(ByVal Text As String)

 ' cette signature de délégué correspond à celle de 
 ' IClient.Failed et permet d'appeler en toute
 ' sécurité cette méthode sur la thread UI 
 Private Delegate Sub FailedDelegate(ByVal e As Exception)

L'interface IClient définit également une méthode Start, que nous appellerons à partir de la thread UI elle-même, de sorte que nous n'avons pas besoin d'un délégué.

Puis nous allons écrire le code qui sera appelé à partir de la thread UI. Le code inclut les deux méthodes constructor, les méthodes Start et Cancel et la propriété Percent. Je les ai regroupées dans un objet Region pour bien montrer qu'elles sont appelées sur la thread UI.

#Region " Code called from UI thread "

 ' Initialise le contrôleur avec un client
 Public Sub New(ByVal Client As IClient)
 mClient = CType(Client, Form)
 End Sub

 ' Cette méthode est appelée par l'UI et
 ' s'exécute sur la thread UI. C'est là que nous allons 
 ' démarrer la thread de travail
 Public Sub Start(Optional ByVal Worker As IWorker = Nothing)
 ' si elle s'exécute déjà, générer une erreur 
 If mRunning Then
 Throw New Exception("Background process already running")
 End If

 mRunning = True

 ' stocke une référence à l'objet worker
 ' et initialise l'objet worker afin qu'il ait
 ' une référence à l'objet Controller
 mWorker = Worker
 mWorker.Initialize(Me)

 ' crée la thread d'arrière-plan pour
 ' le traitement d'arrière-plan
 Dim backThread As New Thread(AddressOf mWorker.Start)

 ' démarre le travail d'arrière-plan
 backThread.Start()

 ' indique au client que le travail d'arrière-plan a démarré
 CType(mClient, IClient).Start(Me)
 End Sub

 ' ce code est appelé par l'UI et 
 ' s'exécute donc sur la thread UI. Il définit simplement
 ' un indicateur pour demander une annulation
 Public Sub Cancel()
 mRunning = False
 End Sub

 ' renvoie le pourcentage exécuté et
 ' est appelé uniquement par la thread UI
 Public ReadOnly Property Percent() As Integer
 Get
 Return mPercent
 End Get
 End Property

#End Region

Le seul code particulier que nous ayons ici est celui de la méthode Start, en vue de créer la thread de travail et de la démarrer :

Dim backThread As New Thread(AddressOf mWorker.Start)

 backThread.Start()

Pour créer la thread, nous transmettons l'adresse de la méthode Start à l'interface IWorker de l'objet Worker. Nous appelons ensuite la méthode Start de l'objet thread pour lancer le traitement. À ce stade, nous devons veiller à ce que l'interface utilisateur n'interagisse pas directement avec l'objet Worker, et inversement.

Notez que la méthode Cancel se contente de définir un indicateur signalant notre intention d'arrêter l'exécution. Il appartient au code de travail de contrôler régulièrement cet indicateur pour savoir s'il doit interrompre l'exécution.

Nous pouvons à présent implémenter le code qui sera appelé par la thread de travail pendant l'exécution de l'objet Worker. Ce code, plus intéressant, doit transmettre les appels de méthodes Display et Completed de la thread de travail à l'interface utilisateur mais en se servant cette fois de la thread UI.

Pour ce faire, nous allons utiliser la méthode Invoke de l'objet Form. Cette méthode accepte un délégué qui pointe sur la méthode que le formulaire doit appeler, ainsi qu'un tableau de type Object contenant les paramètres nécessaires à la méthode.

La méthode Invoke n'appelle pas directement la méthode sur le formulaire, mais demande à celui-ci de renverser la situation et d'appeler la méthode en utilisant la thread UI du formulaire. Tout se passe en arrière-plan, avec l'envoi d'un message Windows au formulaire. Ceci signifie que le formulaire obtient ces appels de méthode de la même façon qu'il reçoit un événement de type clic (click) ou frappe de touche (keypress) du système d'exploitation.

En réalité, ces détails ont peu d'importance. Le résultat est que la méthode Invoke déclenche un processus qui fait en sorte que le formulaire exécute la méthode sur sa thread UI, ce qui est le but recherché.

Là encore, ce code est incorporé dans un objet Region pour bien montrer qu'il est appelé sur la thread de travail :

#Region " Code called from the worker thread "

 ' code appelé à partir de la thread de travail pour mettre à jour l'affichage
 ' ceci déclenche un appel de méthode passé à l'UI avec le 
 ' texte d'état - et cet appel est fait sur la
 ' thread UI 
 Private Sub Display(ByVal Text As String) _
 Implements IController.Display

 Dim disp As New DisplayDelegate( _
 AddressOf CType(mClient, IClient).Display)
 Dim ar() As Object = {Text}

 ' appelle le formulaire client sur la thread UI
 ' pour mettre à jour l'affichage
 mClient.BeginInvoke(disp, ar)

 End Sub

 ' code appelé à partir de la thread de travail pour indiquer l'échec
 ' ceci déclenche un appel de méthode à l'UI avec 
 ' l'objet exception - et cet appel est effectué sur 
 ' la thread UI
 Private Sub Failed(ByVal e As Exception) _
 Implements IController.Failed

 Dim disp As New FailedDelegate(_
 AddressOf CType(mClient, IClient).Failed)
 Dim ar() As Object = {e}

 ' appelle le formulaire client sur la thread UI
 ' pour signaler l'échec
 mClient.Invoke(disp, ar)

 End Sub

 ' code appelé à partir de la thread de travail pour indiquer le % d'exécution
 ' cette valeur va au contrôleur, où elle peut être lue 
 ' par l'UI si nécessaire
 Private Sub SetPercent(ByVal Percent As Integer) _
 Implements IController.SetPercent

 mPercent = Percent

 End Sub

 ' code appelé à partir de la thread de travail pour indiquer l'achèvement
 ' nous passons aussi un paramètre pour indiquer si nous
 ' avons réellement terminé ou si nous avons annulé l'opération
 ' l'appel à l'UI est fait sur la thread UI 
 Private Sub Completed(ByVal Cancelled As Boolean) _
 Implements IController.Completed

 mRunning = False
 Dim comp As New CompletedDelegate( _
 AddressOf CType(mClient, IClient).Completed)
 Dim ar() As Object = {Cancelled}

 ' appelle le formulaire client sur la thread UI
 ' pour indiquer l'achèvement
 mClient.Invoke(comp, ar)

 End Sub

 ' indique si l'exécution se poursuit ou 
 ' si une annulation a été demandée
 ' ce code est appelé sur la thread de travail afin que 
 ' le code de travail sache s'il doit s'arrêter 
 ' de façon ordonnée
 Private ReadOnly Property Running() As Boolean _
 Implements IController.Running
 Get
 Return mRunning
 End Get
 End Property

#End Region

Les méthodes Failed et Completed utilisent la méthode Invoke du formulaire. Par exemple, la méthode Failed effectue le traitement suivant :

Dim disp As New FailedDelegate(_
 AddressOf CType(mClient, IClient).Failed)
 Dim ar() As Object = {e}

 ' appelle le formulaire client sur la thread UI 
 ' pour indiquer un échec
 mClient.Invoke(disp, ar)

Tout d'abord, nous créons un délégué qui pointe sur la méthode Failed du formulaire client à partir de son interface IClient. Ensuite, nous déclarons un tableau de type object contenant la valeur du paramètre que nous passons à la méthode. Enfin, nous appelons la méthode Invoke du formulaire client en transmettant à ce dernier le pointeur délégué et le tableau de paramètres.

Le formulaire appelle cette méthode avec ces paramètres sur la thread UI, qui peut, en toute sécurité, l'exécuter afin de mettre à jour l'affichage.

L'ensemble du processus se déroule de façon synchrone, ce qui veut dire que la thread de travail est bloquée pendant l'appel au formulaire. Si le blocage de la thread de travail pour envoyer un message d'erreur ou d'exécution terminée ne pose pas vraiment problème, il vaut mieux en revanche éviter cette situation pour l'affichage de la moindre information d'état.

Pour cette raison, la méthode Display utilise BeginInvoke de préférence à Invoke. Avec BeginInvoke, l'appel de méthode sur le formulaire s'effectue en mode asynchrone, de sorte que la thread de travail peut continuer de s'exécuter sans attendre que la méthode d'affichage du formulaire soit terminée :

Dim disp As New DisplayDelegate( _
 AddressOf CType(mClient, IClient).Display)
 Dim ar() As Object = {Text}

 ' appelle le formulaire client sur la thread UI 
 ' pour mettre à jour l'affichage

mClient.BeginInvoke(disp, ar)

L'utilisation de BeginInvoke de cette manière permet de maintenir les performances de la thread de travail en évitant tout blocage.

Contrôle ActivityBar

Pour finir, nous allons créer le contrôle ActivityBar, ou barre d'activité, qui affiche les points animés.

Ajoutez un contrôle User au projet nommé ActivityBar.

Redimensionnez-le afin qu'il mesure 110 de large sur 20 de haut. Vous pouvez faire glisser les bordures ou définir la propriété Size dans la fenêtre des propriétés.

Le reste va s'effectuer dans le cadre du code. Pour créer une série animée de « points lumineux » qui clignotent à l'écran, nous allons utiliser plusieurs contrôles PictureBox avec un contrôle Timer. Chaque fois que le contrôle Timer s'éteint, le contrôle PictureBox s'allume en vert, et celui qui était vert prend la couleur de fond du formulaire.

À partir de l'onglet Windows Forms de la boîte à outils, placez un contrôle Timer dans le formulaire et renommez-le en tmAnim. Fixez aussi la propriété Interval à 300 pour définir une vitesse d'animation correcte.

Notez au passage qu'il existe un autre contrôle Timer dans l'onglet Components. Il s'agit d'un minuteur multithread. En d'autres termes, il déclenche son événement Elapsed sur une thread d'arrière-plan et non sur la thread UI comme le minuteur Windows Forms. Son utilisation est en général inefficace si vous créez une interface utilisateur, puisque le code de l'événement Elapsed ne peut pas interagir directement avec notre UI.

Ajoutez ensuite le code suivant concernant le contrôle :

Private mBoxes As New ArrayList()
 Private mCount As Integer

 Private Sub ActivityBar_Load(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles MyBase.Load

 Dim index As Integer

 If mBoxes.Count = 0 Then
 For index = 0 To 6
 mBoxes.Add(CreateBox(index))
 Next
 End If
 mCount = 0

 End Sub

 Private Function CreateBox(ByVal index As Integer) As PictureBox
 Dim box As New PictureBox()

 With box
 SetPosition(box, index)
 .BorderStyle = BorderStyle.Fixed3D
 .Parent = Me
 .Visible = True
 End With
 Return box
 End Function

 Private Sub GrayDisplay()
 Dim index As Integer

 For index = 0 To 6
 CType(mBoxes(index), PictureBox).BackColor = Me.BackColor
 Next
 End Sub

 Private Sub SetPosition(ByVal Box As PictureBox, ByVal Index As Integer)
 Dim left As Integer = CInt(Me.Width / 2 - 7 * 14 / 2)
 Dim top As Integer = CInt(Me.Height / 2 - 5)

 With Box
 .Height = 10
 .Width = 10
 .Top = top
 .Left = left + Index * 14
 End With
 End Sub

 Private Sub tmAnim_Tick(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles tmAnim.Tick

 CType(mBoxes((mCount + 1) Mod 7), PictureBox).BackColor = _
 Color.LightGreen
 CType(mBoxes(mCount Mod 7), PictureBox).BackColor = Me.BackColor

 mCount += 1
 If mCount > 6 Then mCount = 0
 End Sub

 Public Sub Start()
 CType(mBoxes(0), PictureBox).BackColor = Color.LightGreen
 tmAnim.Enabled = True
 End Sub

 Public Sub [Stop]()
 tmAnim.Enabled = False
 GrayDisplay()
 End Sub

 Private Sub ActivityBar_Resize(ByVal sender As Object, _
 ByVal e As System.EventArgs) Handles MyBase.Resize

 Dim index As Integer

 For index = 0 To mBoxes.Count - 1
 SetPosition(CType(mBoxes(index), PictureBox), index)
 Next
 End Sub

L'événement Load du formulaire crée les contrôles PictureBox et les place dans un tableau, ce qui permet de les parcourir en boucle. L'événement Tick du contrôle Timer parcourt l'affichage en boucle afin que les points s'allument en vert les uns après les autres.

Le processus est lancé par la méthode Start et arrêté par l'événement Stop. Stop étant un mot réservé, le nom de la méthode est placé entre crochets, comme suit : [Stop]. Non seulement la méthode Stop arrête le minuteur, mais elle met aussi en grisé toutes les cases ou zones à l'écran pour indiquer à l'utilisateur l'absence d'activité.

Création d'une classe Worker

Plus haut dans cet article, nous avons examiné une classe Worker simple. Maintenant que nous avons défini l'interface IWorker, nous pouvez améliorer cette classe en vue d'utiliser l'objet Controller créé.

Tout d'abord, créons le fichier Background.dll. Cette étape est importante car sans elle, le contrôle ActivityBar n'apparaîtrait pas dans la boîte à outils lors de la création de notre formulaire de test.

Ajoutez un projet Windows Forms Application nommé bgTest. Définissez-le comme projet de démarrage en cliquant avec le bouton droit de la souris dans l'Explorateur de solutions et en sélectionnant l'option de menu appropriée.

Utilisez ensuite l'onglet Projets de la boîte de dialogue Ajouter une référence pour ajouter une référence au projet Background.

Puis ajoutez une classe appelée Worker. Le code est le même que précédemment, avec en plus une partie permettant de mettre en œuvre l'interface IWorker (cette partie est ici en gras) :



Imports Background


Public Class Worker

Implements IWorker


Private mController As IController

 Private mInner As Integer
 Private mOuter As Integer

 Public Sub New(ByVal InnerSize As Integer, ByVal OuterSize As Integer)
 mInner = InnerSize
 mOuter = OuterSize
 End Sub


' code appelé par le contrôleur afin d'obtenir


' une référence au contrôleur


Private Sub Init(ByVal Controller As IController) _


Implements IWorker.Initialize


mController = Controller


End Sub


Private Sub Work() Implements IWorker.Start

 Dim innerIndex As Integer
 Dim outerIndex As Integer

 Dim value As Double


Try

 For outerIndex = 0 To mOuter

If mController.Running Then


mController.Display("Outer loop " & outerIndex & " starting")


mController.SetPercent(CInt(outerIndex / mOuter * 100))


Else


' une annulation a été demandée


mController.Completed(True)


Exit Sub


End If


 For innerIndex = 0 To mInner
 ' exécution de calculs
 value = Math.Sqrt(CDbl(innerIndex - outerIndex))
 Next
 Next

mController.SetPercent(100)


mController.Completed(False)


Catch e As Exception


mController.Failed(e)


End Try

 End Sub
End Class

Nous avons ajouté une méthode Init qui implémente IWorker.Initialize. Le composant Controller appelle cette méthode, ce qui nous permet d'avoir ultérieurement une référence à l'objet Controller.

Nous avons aussi modifié la méthode Work afin qu'elle soit de type Private, afin d'implémenter la méthode IWorker.Start. Cette méthode s'exécutera sur la thread de travail.

La méthode Work a été améliorée et utilise désormais un bloc Try..Catch qui intercepte les erreurs et les renvoie à l'interface utilisateur en utilisant la méthode Failed sur le Controller.

Imaginons que notre code s'exécute : nous appelons les méthodes Display et SetPercent de l'objet Controller pour mettre à jour l'état et le pourcentage d'exécution pendant l'exécution du code.

Nous devons aussi contrôler régulièrement la valeur de la propriété Running de l'objet Controller afin de voir si une annulation a été demandée. Si c'est le cas, nous arrêtons le traitement en indiquant comme raison l'existence d'une demande d'annulation.

Création d'un formulaire d'affichage

Pour finir, nous pouvons créer un formulaire qui démarre et éventuellement annule le processus d'arrière-plan. Ce formulaire affichera également des informations d'activité et d'état.

Ouvrez le concepteur pour créer le formulaire Form1 et ajoutez deux boutons (btnStart et btnRequestCancel), deux libellés (Label1 et Label2) une barre de progression (ProgressBar1) et notre barre d'activité (ActivityBar1), comme indiqué dans la Figure 7.

Figure 7. Disposition des contrôles de Form1

Figure 7. Disposition des contrôles de Form1

Le formulaire doit implémenter l'interface IClient afin que l'objet Controller puisse interagir avec lui :



Imports Background


Public Class Form1
 Inherits System.Windows.Forms.Form


Implements IClient


Le formulaire a aussi besoin d'un objet Controller et d'un indicateur pour savoir si le traitement en arrière-plan est en cours ou terminé.

Private mController As New Controller(Me)
 Private mActive As Boolean

Ajoutons ensuite des méthodes qui mettent en œuvre l'interface définie par IClient. Une fois de plus, je préfère regrouper ces méthodes pour bien montrer qu'elles implémentent une interface secondaire :

#Region " IClient "

 Private Sub TaskStarted(ByVal Controller As Controller) _
 Implements IClient.Start

 mActive = True
 Label1.Text = "Starting"
 Label2.Text = "0%"
 ProgressBar1.Value = 0
 ActivityBar1.Start()

 End Sub

 Private Sub TaskStatus(ByVal Text As String) _
 Implements IClient.Display

 Label1.Text = Text
 Label2.Text = CStr(mController.Percent) & "%"
 ProgressBar1.Value = mController.Percent

 End Sub

 Private Sub TaskFailed(ByVal e As Exception) _
 Implements IClient.Failed

 ActivityBar1.Stop()
 Label1.Text = e.Message
 MsgBox(e.ToString)
 mActive = False

 End Sub

 Private Sub TaskCompleted(ByVal Cancelled As Boolean) _
 Implements IClient.Completed

 Label1.Text = "Completed"
 Label2.Text = CStr(mController.Percent) & "%"
 ProgressBar1.Value = mController.Percent
 ActivityBar1.Stop()
 mActive = False

 End Sub

#End Region

Vous pouvez constater que dans ce code, rien ne concerne le threading. Chaque partie vise à réagir en fonction des informations d'état qui nous sont communiquées sur le traitement d'arrière-plan. Dans chaque cas, nous mettons à jour l'affichage en vue d'indiquer l'état du processus, le pourcentage d'exécution (par un message et via la barre de progression ProgressBar), ainsi que pour arrêter et démarrer le contrôle ActivityBar.

L'indicateur mActive est important. Si l'utilisateur ferme le formulaire pendant que la thread de travail est active, notre application peut se bloquer ou devenir instable. Pour éviter cela, nous pouvons intercepter l'événement Closing du formulaire et annuler la tentative de fermeture si le processus d'arrière-plan est actif.

Private Sub Form1_Closing(ByVal sender As Object, _
 ByVal e As System.ComponentModel.CancelEventArgs) _
 Handles MyBase.Closing

 e.Cancel = mActive

 End Sub

Nous pourrions aussi choisir de déclencher une annulation, mais tout dépend des besoins spécifiques de notre application.

Il reste à présent à implémenter les événements Click correspondant aux boutons.

Private Sub btnStart_Click(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles btnStart.Click

 mController.Start(New Worker(2000000, 100))

 End Sub

 Private Sub btnStop_Click(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles btnStop.Click

 Label1.Text = "Cancelling ..."
 mController.Cancel()

 End Sub

Le bouton Démarrer appelle simplement la méthode Start de l'objet Controller en transmettant une instance de l'objet Worker.

Sur votre machine, peut-être devrez-vous ajuster les valeurs utilisées pour initialiser l'objet Worker afin d'obtenir des résultats appropriés. Les valeurs fournies ici conviennent à un ordinateur P3/450 biprocesseur. Mais n'oublions pas que notre exemple est un test. Dans la réalité, l'objet Worker implémenterait un processus plus significatif et encore plus long à s'exécuter.

Le bouton Cancel appelle la méthode Cancel de l'objet Controller et met à jour l'affichage pour indiquer qu'une annulation a été demandée. N'oubliez qu'il s'agit juste d'une demande d'annulation et il peut donc s'écouler un certain laps de temps avant que le code ne s'arrête vraiment. Il faut aussi signaler à l'utilisateur que son clic de souris a été remarqué par le système.

Nous pouvons à présent exécuter l'application. Lorsque l'utilisateur clique sur le bouton Démarrer, la thread Worker doit démarrer et l'affichage est mis à jour à mesure que le code se déroule. Vous pouvez déplacer le formulaire à l'écran et le manipuler puisque la thread UI reste généralement inactive et prête à toute interaction de votre part.

En même temps, la thread Worker exécute plusieurs opérations en arrière-plan, en transmettant régulièrement des informations d'état à la thread UI qui les affiche.

Conclusion

Le multithreading est un instrument puissant que nous pouvons utiliser chaque fois qu'il s'agit d'exécuter une tâche de longue durée. Il permet l'exécution d'un code de travail sans bloquer l'interface utilisateur. En même temps, le multithreading peut s'avérer incroyablement difficile à utiliser et plus complexe encore en cas de débogage.

Nous devons toujours chercher à définir pour chaque thread de travail un ensemble isolé de données sur lequel elle peut opérer, même si cette démarche se révèle difficile. Le plus simple consiste à créer, pour chaque thread, un objet contenant les données sur lesquelles elle devra agir et le code chargé d'effectuer le travail.

En implémentant une structure organisée servant d'intermédiaire entre les threads de travail et notre thread UI, nous pouvons considérablement simplifier l'écriture d'un code de travail multithread et d'une interface utilisateur pour le contrôler. Dans cet article, j'ai décrit une structure de ce type, que vous pouvez utiliser telle quelle ou adapter en fonction des besoins de votre application.



Dernière mise à jour le jeudi 5 décembre 2002



Pour en savoir plus
Microsoft réalise une enquête en ligne pour recueillir votre opinion sur le site Web de MSDN. Si vous choisissez d’y participer, cette enquête en ligne vous sera présentée lorsque vous quitterez le site Web de MSDN.

Si vous souhaitez y participer,
Afficher:
© 2014 Microsoft