Exporter (0) Imprimer
Développer tout
4 sur 5 ont trouvé cela utile - Évaluez ce sujet

Programmation multithread avec Visual Basic .NET

Visual Studio .NET 2003

Robert Burns
L'équipe Visual Studio
Microsoft Corporation

Résumé : le .NET Framework offre de nouvelles classes qui facilitent la création d'applications multithread. Cet article décrit l'utilisation des techniques de programmation multithread avec Visual Basic® .NET pour développer des applications plus efficaces et plus réactives.

Sommaire

Introduction
Les avantages du traitement multithread
Création de nouveaux threads
Synchronisation des threads
Minuterie de thread
Annulation des tâches
Conclusions

Introduction

En règle générale, les développeurs en Visual Basic créent des applications synchrones dans lesquelles les tâches du programme s'exécutent de façon séquentielle. Les applications multithread sont en principe plus efficaces du fait de l'exécution plus ou moins simultanée de plusieurs tâches, mais leur création avec les versions précédentes de Visual Basic s'est toujours révélée difficile.

Les programmes multithread sont possibles grâce à une fonction du système d'exploitation "multitâche" et qui simule la possibilité d'exécuter plusieurs applications simultanément. Bien que la plupart des ordinateurs personnels ne soient dotés que d'un processeur, les systèmes d'exploitation modernes permettent le traitement multitâche en répartissant le temps processeur entre plusieurs parties de code exécutable nommées threads. Un thread peut représenter une application complète, mais il ne représente souvent qu'une partie de l'application exécutable séparément. Le système d'exploitation alloue le temps de traitement à chaque thread en se basant sur des facteurs tels que la priorité des threads et le temps écoulé depuis la dernière exécution d'un thread. L'utilisation de plusieurs threads peut considérablement améliorer les performances lorsque des tâches longues, telles que l'entrée et la sortie de fichiers, sont exécutées.

Il convient cependant de ne pas oublier certains éléments. Bien que l'utilisation de plusieurs threads puisse améliorer les performances, à chaque thread est associé un coût en terme de mémoire supplémentaire nécessaire pour le créer et de temps processeur pour l'exécuter. En créant un trop grand nombre de threads, vous risquez de réduire les performances de l'application. Lorsque vous concevez des applications multithread, vous devez évaluer les avantages liés à l'ajout de threads, sans oublier les coûts qu'ils suscitent.

Le traitement multitâche fait partie des systèmes d'exploitation depuis déjà un certain temps. Cependant, jusqu'à ces derniers temps, les programmeurs en Visual Basic ne pouvaient exécuter des tâches multithread qu'en utilisant des fonctions non documentées, ou indirectement en utilisant des composants COM ou des parties asynchrones du système d'exploitation. Le .NET Framework offre la prise en charge complète du développement des applications multithread dans l'espace de noms System.Threading.

Cet article décrit certains avantages du traitement multithread et l'utilisation de Visual Basic .NET pour développer des applications multithread. Certes, Visual Basic .NET et le .NET Framework simplifient le développement d'applications multithread ; toutefois, cet article s'adresse à des développeurs de niveau intermédiaire ou expérimenté et à ceux qui passent des versions antérieures de Visual Basic à Visual Basic .NET. Si vous êtes encore profane dans l'utilisation de Visual Basic .NET, lisez d'abord l'article Visual Basic Language Tour (en anglais).

Même si le présent article n'a pas pour objectif d'aborder dans le détail la programmation multithread, vous trouverez à la fin des références à d'autres documents.

Les avantages du traitement multithread

Bien que les applications synchrones soient plus faciles à développer, elles sont souvent moins efficaces que les applications multithread car chaque tâche doit attendre la fin de la précédente pour démarrer. Si la réalisation d'une tâche synchrone dure plus longtemps que prévu, votre application semble ne plus répondre. Pour sa part, le traitement multithread peut exécuter plusieurs procédures simultanément. Par exemple, une application de traitement de texte va vérifier l'orthographe pendant que vous continuez de travailler sur le document. En décomposant les programmes en tâches indépendantes, les applications multithread améliorent sensiblement les performances, et ce pour les raisons suivantes :

  • En mode multithread, vos programmes sont plus réactifs car l'interface utilisateur reste active alors que d'autres tâches se poursuivent.
  • Les tâches qui ne sont pas en cours d'exécution libèrent du temps processeur pour d'autres tâches.
  • Celles qui consomment un temps processeur important peuvent, à intervalles réguliers, en libérer pour d'autres tâches.
  • À tout moment, il est possible d'arrêter des tâches.
  • Vous pouvez augmenter ou abaisser la priorité de tâches individuelles pour optimiser les performances.

La décision de créer explicitement une application multithread dépend de plusieurs facteurs. Ainsi, le traitement multithread convient mieux lorsque :

  • Des tâches longues ou qui mobilisent le processeur bloquent l'interface utilisateur.
  • Des tâches individuelles doivent attendre une ressource externe, telle qu'un fichier distant ou une connexion Internet.

Par exemple, imaginons un "robot" Internet, une application qui suit les liens sur les pages Web et télécharge les fichiers qui correspondent à des critères spécifiques. Ce type d'application peut soit télécharger les fichiers l'un après l'autre de façon synchrone, soit utiliser le traitement multithread pour télécharger plusieurs fichiers en même temps. L'approche multithread s'avère souvent beaucoup plus efficace que l'approche synchrone en raison de la possibilité de télécharger les fichiers, même si certains threads reçoivent des réponses lentes de serveurs Web distants.

Création de nouveaux threads

Le moyen le plus direct pour créer un thread consiste à créer une nouvelle instance de la classe de thread et à utiliser l'instruction AddressOf pour transmettre un délégué à la procédure que vous voulez exécuter. Par exemple, le code suivant exécute une sous-procédure nommée

SomeTask
en tant que thread distinct.
Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask)
Thread1.Start
' Ici, le code s'exécute immédiatement.

Rien d'autre n'est nécessaire à la création et au démarrage d'un thread. Le code qui suit un appel à la méthode Start du thread s'exécute immédiatement sans attendre la fin du thread précédent.

Le tableau ci-dessous répertorie certaines méthodes utilisables pour contrôler des threads individuels.

Méthode Action
Start Démarre un thread.
Sleep Interrompt un thread pendant un temps déterminé.
Suspend Interrompt un thread lorsqu'il atteint un passage sûr.
Abort Arrête un thread lorsqu'il atteint un passage sûr.
Resume Redémarre un thread interrompu.
Join Place le thread courant en attente jusqu'à la fin d'un autre thread. Utilisée avec une valeur de temporisation, cette méthode renvoie la valeur True si le thread se termine dans le temps imparti.

La plupart de ces méthodes sont explicites, mais il est possible que vous ne connaissiez pas le concept de passage sûr. Les passages sûrs sont des endroits du code où le Common Language Runtime peut effectuer automatiquement et en toute sécurité l'opération garbage collection qui consiste à libérer des variables inutilisées et à récupérer de la mémoire. Lorsque vous appelez les méthodes Abort ou Suspend d'un thread, le Common Language Runtime analyse le code et détermine le meilleur point d'arrêt possible pour le thread.

Les threads contiennent également des propriétés utiles, récapitulées dans le tableau ci-dessous :

Propriété Valeur
IsAlive Contient la valeur True si un thread est actif.
IsBackground Récupère ou définit une valeur booléenne indiquant si un thread est ou doit être un thread d'arrière-plan. Les threads d'arrière-plan sont comme les threads de premier plan, mais ils n'empêchent pas un processus d'arriver à son terme. Lorsque tous les threads de premier plan appartenant à un processus ont pris fin, le Common Language Runtime termine le processus en appelant la méthode Abort sur les threads d'arrière-plan toujours actifs.
Name Récupère ou définit le nom d'un thread. S'utilise généralement lors du débogage pour détecter des threads individuels.
Priority Récupère ou définit une valeur utilisée par le système d'exploitation pour établir la priorité des threads.
ApartmentState Récupère ou définit le modèle utilisé pour un thread donné. Les modèles de thread sont importants lorsqu'un thread appelle un code non géré.
ThreadState Contient une valeur qui décrit le ou les états d'un thread.

Les propriétés et les méthodes de thread sont utiles pour créer et gérer des threads. Leur utilisation pour contrôler et coordonner les threads est décrite dans la section de cet article relative à la synchronisation des threads.

Arguments de thread et valeurs de retour

L'appel de méthode de l'exemple précédent ne peut contenir aucun paramètre ni valeur de retour. Cette limitation est l'un des principaux inconvénients de ce mode de création et d'exécution des threads. Cependant, vous pouvez fournir et retourner des arguments pour les procédures exécutées sur des threads distincts en les incorporant dans une classe ou une structure.

Class TasksClass
 Friend StrArg As String
 Friend RetVal As Boolean
 Sub SomeTask()
 ' Utiliser le champ StrArg comme argument.
 MsgBox("StrArg contient la chaîne " & StrArg)
 RetVal = True ' Définir une valeur de retour dans l'argument retourné.
 End Sub
End Class
' Pour utiliser la classe, définissez les propriétés
' ou les champs qui stockent des paramètres
' puis appelez de façon asynchrone les méthodes si nécessaire.
Sub DoWork()
 Dim Tasks As New TasksClass()
 Dim Thread1 As New System.Threading.Thread( _
 AddressOf Tasks.SomeTask)
 Tasks.StrArg = "Some Arg" ' Définir un champ utilisé comme argument
 Thread1.Start() ' Démarrer le nouveau thread.
 Thread1.Join() ' Attendre la fin du thread 1.
 ' Afficher la valeur de retour.
 MsgBox("Le thread 1 a retourné la valeur " & Tasks.RetVal)
End Sub

La création et la gestion manuelle des threads conviennent mieux aux applications dans lesquelles vous voulez disposer d'un contrôle fin des détails, tels que la priorité des threads et le modèle de thread. Mais comme vous l'imaginez aisément, si les threads sont nombreux, ce type de gestion risque de s'avérer difficile. Pour réduire la complexité liée à l'utilisation de nombreux threads, il est possible d'utiliser des pools de threads.

Pools de threads

Les pools de threads offrent une forme de traitement multithread où les tâches sont mises en file d'attente et démarrent automatiquement lorsque les threads sont créés. Avec les pools de threads, vous appelez la méthode Threadpool.QueueUserWorkItem avec un délégué pour la procédure à exécuter, et Visual Basic .NET crée le thread puis exécute votre procédure. L'exemple ci-après montre comment utiliser les pools de threads pour démarrer plusieurs tâches.

Sub DoWork()
 Dim TPool As System.Threading.ThreadPool
 ' Mettre une tâche en file d'attente
 TPool.
QueueUserWorkItem(New System.Threading.WaitCallback _
 (AddressOf SomeLongTask))
 ' Mettre une autre tâche en file d'attente
 TPool.
QueueUserWorkItem
(New System.Threading.WaitCallback _
 (AddressOf AnotherLongTask))
End Sub

Les pools de threads sont utiles pour démarrer plusieurs tâches distinctes sans définir individuellement les propriétés de chaque thread. Chaque thread démarre avec une taille de pile et une priorité par défaut. Par défaut, un processeur système peut exécuter jusqu'à 25 threads regroupés en pool. Il est possible de mettre en file d'attente des threads supplémentaires, mais ceux-ci ne démarreront pas tant que les autres ne seront pas terminés.

Les pools de threads ont l'avantage de permettre la transmission d'arguments à la procédure de la tâche dans un objet d'état (state). Si la procédure que vous appelez nécessite plusieurs arguments, vous pouvez convertir une structure ou une instance de classe en type de données Object.

Paramètres et valeurs de retour

Le renvoi de valeurs par un thread appartenant à un pool est un peu délicat. Le mécanisme standard de renvoi des valeurs à partir d'un appel de fonction n'est pas autorisé car les procédures Sub sont le seul type qu'il est possible de mettre en file d'attente dans un pool de threads. Une solution consiste à incorporer les paramètres, les valeurs de retour et les méthodes dans une classe wrapper comme indiqué dans la section Arguments de thread et valeurs de retour. Toutefois, il est plus simple d'utiliser la variable d'objet d'état facultative ByVal de la méthode QueueUserWorkItem. Si vous utilisez cette variable pour transmettre une référence à l'instance d'une classe, les membres de cette instance peuvent être modifiés par le thread du pool et utilisés comme valeurs de retour. Au premier abord, il peut sembler curieux de modifier un objet référencé à l'aide d'une variable transmise par valeur. Or, la chose est possible car seule la référence à l'objet est transmise par une valeur. Lorsque vous modifiez des membres de l'objet référencé par la référence de l'objet, les changements s'appliquent à l'instance de la classe actuelle.

L'emploi de structures pour retourner des valeurs dans des objets state est impossible. Les structures étant des types de valeurs, les changements générés par le processus asynchrone ne modifient pas les membres de la structure d'origine. Utilisez des structures pour fournir des paramètres lorsque vous n'avez pas besoin de valeurs de retour.

Friend Class StateObj
 Friend StrArg As String
 Friend IntArg As Integer
 Friend RetVal As String
End Class

Sub ThreadPoolTest()
 Dim TPool As System.Threading.ThreadPool
 Dim StObj1 As New StateObj()
 Dim StObj2 As New StateObj()
 ' Définir certains champs agissant comme paramètres dans l'objet state.
 StObj1.IntArg = 10
 StObj1.StrArg = "Une chaîne"
 StObj2.IntArg = 100
 StObj2.StrArg = "Une autre chaîne"
 ' Mettre une tâche en file d'attente
 TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
 (AddressOf SomeOtherTask), StObj1)
 ' Mettre une autre tâche en file d'attente
 TPool.QueueUserWorkItem(New System.Threading.WaitCallback _
 (AddressOf AnotherTask), StObj2)
End Sub

Sub SomeOtherTask(ByVal StateObj As Object)
 ' Utiliser les champs de l'objet state comme arguments.
 Dim StObj As StateObj
 StObj = CType(StateObj, StateObj) ' Convertir vers le type correct.
 MsgBox("StrArg contient la chaîne " & StObj.StrArg)
 MsgBox("IntArg contient le nombre " & CStr(StObj.IntArg))
 ' Utiliser un champ comme valeur de retour.
 StObj.RetVal = "Retourner la valeur à partir de SomeOtherTask"
End Sub

Sub AnotherTask(ByVal StateObj As Object)
 ' Utiliser les champs de l'objet state comme arguments.
 ' L'objet state est transmis en tant qu'objet.
 ' Sa conversion dans son type spécifique en facilite l'utilisation.
 Dim StObj As StateObj
 StObj = CType(StateObj, StateObj)
 MsgBox("StrArg contient la chaîne " & StObj.StrArg)
 MsgBox("IntArg contient le nombre " & CStr(StObj.IntArg))
 ' Utiliser un champ comme valeur de retour.
 StObj.RetVal = "Retourner la valeur à partir de AnotherTask"
End Sub

Le Common Language Runtime crée automatiquement des threads pour les tâches d'un pool de threads mises en file d'attente et libère ces ressources une fois ces tâches terminées. Il n'existe pas de moyen simple pour annuler une tâche mise en file d'attente. Les threads ThreadPool s'exécutent toujours selon le modèle multithread cloisonné (MTA). Si vous souhaitez mettre en oeuvre le modèle mono-thread (STA), vous devez les créer manuellement.

Synchronisation des threads

La synchronisation offre un compromis entre la nature non structurée de la programmation multithread et le caractère structuré du traitement synchrone.

Les techniques de synchronisation permettent de :

  • Contrôler de façon explicite l'ordre d'exécution du code lorsque les tâches doivent s'effectuer dans un ordre déterminé

    ou

  • D'éviter d'éventuels problèmes lorsque deux threads partagent la même ressource au même moment

Par exemple, utilisez la synchronisation pour faire attendre une procédure d'affichage jusqu'à ce qu'une procédure de récupération de données exécutée sur un autre thread soit terminée.

Il existe deux approches de la synchronisation : l'interrogation et l'utilisation d'objets de synchronisation. L'interrogation vérifie plusieurs fois le statut d'un appel asynchrone depuis une boucle. Il s'agit du mode de gestion des threads le moins efficace car il gaspille les ressources en vérifiant régulièrement le statut des différentes propriétés des threads.

Par exemple, la propriété IsAlive permet de vérifier si un thread s'est arrêté. Utilisez cette propriété avec précaution car un thread actif n'est pas nécessairement en cours d'exécution. La propriété ThreadState du thread vous permet d'obtenir des informations plus détaillées sur son état. Les threads pouvant avoir plusieurs états à un instant donné, la valeur stockée dans la propriété ThreadState peut être une combinaison des valeurs de l'énumération System.Threading.Threadstate. En conséquence, il convient de vérifier attentivement tous les états d'un thread lors d'une interrogation. Par exemple, si un thread n'a pas l'état Running, il se peut qu'il soit terminé. Mais il peut également être suspendu ou interrompu.

Comme vous l'imaginez, avec l'interrogation, le contrôle de l'ordre d'exécution des threads s'effectue au détriment de certains avantages du traitement multithread. Une approche plus efficace pour contrôler les threads sera donc d'utiliser la méthode Join. La méthode Join fait attendre la procédure appelante jusqu'à l'achèvement d'un thread ou jusqu'à la fin du délai de temporisation éventuellement défini. Le nom, join, s'appuie sur l'idée que la création d'un nouveau thread constitue un embranchement dans le chemin d'exécution. La méthode Join permet de fusionner dans un même thread des chemins d'exécution séparés.

Thread

Figure 1 Thread

Il convient de souligner que la méthode Join est un appel synchrone ou bloquant. Lorsque vous appelez la méthode Join ou la méthode Wait d'un descripteur d'attente, la procédure appelante s'interrompt et attend le signal de fin du thread.

Sub JoinThreads()
 Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask)
 Thread1.Start()
 Thread1.Join() ' Attendre la fin du thread.
 MsgBox("Le thread est terminé")
End Sub

Ces moyens simples de contrôle des threads, utiles lorsque vous gérez un petit nombre de threads, sont difficiles à utiliser avec des projets de taille importante. La section suivante décrit certaines techniques avancées de synchronisation des threads.

Techniques de synchronisation avancées

Les applications multithread utilisent fréquemment des descripteurs d'attente et des objets monitor pour synchroniser les threads. Le tableau ci-après présente certaines classes .NET Framework utilisables à cet effet.

Classe Objectif
AutoResetEvent Descripteur d'attente qui informe un ou plusieurs threads en attente qu'un événement s'est produit. Lorsqu'un thread en attente est libéré, AutoResetEvent change automatiquement l'état à "signalé".
Interlocked Permet les opérations atomiques pour les variables partagées par plusieurs threads.
ManualResetEvent Descripteur d'attente qui informe un ou plusieurs threads en attente qu'un événement s'est produit. L'état d'un événement réinitialisé manuellement reste "signalé" jusqu'à ce que la méthode Reset le passe à l'état "non signalé". De même, l'état reste "non signalé" jusqu'à ce que la méthode Set le passe à l'état "signalé". Lorsque l'état d'un objet est "signalé", plusieurs threads en attente ou sur le point d'appeler une des fonctions d'attente peuvent être libérés.
Monitor Offre un mécanisme qui synchronise l'accès aux objets. Les applications Visual Basic .NET appellent SyncLock pour utiliser les objets monitor.
Mutex Descripteur d'attente utilisable pour la synchronisation entre processus.
ReaderWriterLock Définit le verrou qui implémente la sémantique écriture simple et lecture multiple.
Timer Offre un mécanisme pour l'exécution de tâches à des intervalles définis.
WaitHandle Encapsule les objets spécifiques d'un système d'exploitation qui attendent l'accès exclusif à des ressources partagées.
Descripteurs d'attente

Les descripteurs d'attente sont des objets qui signalent l'état d'un thread à un autre thread. Les threads s'en servent par exemple pour avertir d'autres threads qu'ils ont besoin d'un accès exclusif à une ressource. Les seconds doivent alors attendre, pour accéder à cette ressource, que le descripteur d'attente ne soit plus utilisé. Les descripteurs d'attente ont deux états, signalé et non signalé. Un descripteur d'attente qui n'appartient à aucun thread a l'état signalé. Un descripteur d'attente qui appartient à un thread a l'état non signalé.

Les threads revendiquent la propriété d'un descripteur d'attente en appelant l'une des méthodes d'attente, telles que WaitOne, WaitAny ou WaitAll. Les méthodes d'attente sont des appels bloquants similaires à la méthode Join d'un thread individuel.

  • Si aucun autre thread n'est propriétaire du descripteur d'attente, l'appel renvoie immédiatement la valeur True, le descripteur d'attente prend l'état "non signalé" et le thread propriétaire du descripteur d'attente poursuit son exécution.
  • Si un thread appelle une des méthodes d'attente du descripteur d'attente alors que ce dernier appartient à un autre thread, le thread appelant devra attendre un certain temps (si un délai a été spécifié) ou indéfiniment (en l'absence de délai) avant que l'autre thread ne libère le descripteur d'attente. Si un délai a été spécifié et que le descripteur d'attente est libéré avant la fin de ce délai, l'appel renvoie la valeur True. Dans le cas contraire, il renvoie False et le thread appelant poursuit son exécution.

Les threads qui sont propriétaires d'un descripteur d'attente appellent la méthode Set lorsqu'ils sont terminés ou qu'ils n'ont plus besoin du descripteur d'attente. Les autres threads peuvent réinitialiser l'état d'un descripteur d'attente à "non signalé" soit en appelant la méthode Reset, soit en appelant WaitOne, WaitAll ou WaitAny et en attendant qu'un thread appelle Set. Les descripteurs AutoResetEvent reprennent automatiquement l'état "non signalé" après la libération d'un thread en attente. Si aucun thread n'est en attente, l'objet event garde l'état "signalé".

Méthode Objectif
WaitOne Accepte un descripteur d'attente comme argument et met en attente le thread appelant jusqu'à ce que le descripteur en cours soit signalé par un autre thread qui appelle Set.
WaitAny Accepte un groupe de descripteurs d'attente comme argument et met en attente le thread appelant jusqu'à ce que l'un des descripteurs spécifiés se signale en appelant Set.
WaitAll Accepte un groupe de descripteurs d'attente comme argument et met en attente le thread appelant jusqu'à ce que tous les descripteurs spécifiés se signalent en appelant Set.
Set Définit l'état du descripteur d'attente spécifié à "signalé" et entraîne la reprise de tous les threads en attente.
Reset Définit l'état de l'événement spécifié à "non signalé".

Il existe trois sortes de descripteurs d'attente couramment utilisés avec Visual Basic .NET : les objets mutex, ManualResetEvent, AutoResetEvent. Les deux derniers sont souvent appelés événements de synchronisation.

Objets mutex

Les objets mutex sont des objets de synchronisation qui ne peuvent appartenir qu'à un thread à la fois. En fait, le nom mutex vient du fait que la propriété des objets mutex est mutuellement exclusive. Les threads revendiquent la propriété de l'objet mutex lorsqu'ils ont besoin d'un accès exclusif à une ressource. Un objet mutex ne pouvant appartenir qu'à un seul thread à la fois, les autres threads doivent attendre pour acquérir l'objet et utiliser la ressource.

La méthode WaitOne oblige le thread appelant à attendre pour devenir propriétaire d'un objet mutex. Lorsqu'un thread possédant un objet mutex prend fin, l'objet prend l'état "signalé" et le thread en attente suivant peut l'obtenir.

Événements de synchronisation

Les événements de synchronisation servent à prévenir les autres threads qu'un événement s'est produit ou qu'une ressource est disponible. Ne vous laissez pas abuser par le mot événement. Les événements de synchronisation, contrairement aux autres événements de Visual Basic, sont en réalité des descripteurs d'attente et, comme eux, ils ont deux états : signalé et non signalé. Les threads qui appellent une méthode d'attente d'un événement de synchronisation doivent attendre qu'un autre thread signale l'événement en appelant la méthode Set. Il existe deux classes d'événement de synchronisation. Certains threads définissent l'état des instances ManualResetEvent à "signalé" en utilisant la méthode Set. D'autres définissent l'état des instances ManualResetEvent à "non signalé" en utilisant la méthode Reset ou lorsque le contrôle retourne à un appel WaitOne en attente. Les instances de la classe AutoResetEvent peuvent également être définies à "signalé" par le biais de la méthode Set, mais elles reprennent automatiquement l'état "non signalé" dès qu'un thread en attente sait que l'événement a pris l'état "signalé".

L'exemple ci-après utilise la classe AutoResetEvent pour synchroniser les tâches d'un pool de threads.

Sub StartTest()
 Dim AT As New AsyncTest()
 AT.StartTask()
End Sub

Class AsyncTest
 Private Shared AsyncOpDone As New _
 System.Threading.AutoResetEvent(False)

 Sub StartTask()
 Dim Tpool As System.Threading.ThreadPool
 Dim arg As String = "SomeArg"
 Tpool.QueueUserWorkItem(New System.Threading.WaitCallback( _
 AddressOf Task), arg) ' Mettre une tâche en file d'attente.
 AsyncOpDone.WaitOne() ' Attendre que le thread appelle la méthode Set.
 MsgBox("Le thread est terminé.")
 End Sub

 Sub Task(ByVal Arg As Object)
 MsgBox("Le thread démarre.")
 System.Threading.Thread.Sleep(4000) ' Attendre 4 secondes.
 MsgBox("L'objet state contient la chaîne " & CStr(Arg))
 AsyncOpDone.Set() ' Signaler que le thread est terminé.
 End Sub
End Class
Objets monitor et SyncLock

Les objets monitor permettent de garantir qu'un bloc de code s'exécute sans être interrompu par le code exécuté sur d'autres threads. En d'autres termes, le code des autres threads ne peut s'exécuter tant que le code du bloc de code synchronisé n'est pas terminé. Le mot clé SyncLock simplifie l'accès aux objets monitor de Visual Basic .NET. Visual C#® .NET utilise le mot clé Lock de la même manière.

Prenons l'exemple d'un programme qui lit des données de façon répétée et asynchrone puis affiche les résultats. Un système d'exploitation en mode multitâche préemptif peut interrompre un thread en cours d'exécution pour donner le temps à un autre thread de s'exécuter. En revanche, sans synchronisation, vous risquez d'obtenir une vue partiellement mise à jour des données si l'objet correspondant est modifié par un autre thread pendant l'affichage des données. L'instruction SyncLock garantit qu'une section du code s'exécutera sans interruption. L'exemple ci-après montre comment utiliser SyncLock pour fournir à la procédure d'affichage un accès exclusif à un objet de données.

Class DataObject
 Public ObjText As String
 Public ObjTimeStamp As Date
End Class

Sub RunTasks()
 Dim MyDataObject As New DataObject()
 ReadDataAsync(MyDataObject)
 SyncLock MyDataObject
 DisplayResults(MyDataObject)
 End SyncLock
End Sub

Sub ReadDataAsync(ByRef MyDataObject As DataObject)
 ' Ajoutez le code pour lire et traiter les données de façon asynchrone.
End Sub

Sub DisplayResults(ByVal MyDataObject As DataObject)
 ' Ajoutez le code pour afficher les résultats.
End Sub

Utilisez SyncLock pour éviter qu'une section du code ne soit interrompue par le code exécuté sur un thread distinct.

Classe Interlocked

Les méthodes de la classe Interlocked permettent d'éviter les problèmes susceptibles d'apparaître lorsque plusieurs threads tentent simultanément de mettre à jour et de comparer la même valeur. Elles permettent en toute sécurité d'incrémenter, de décrémenter, d'échanger et de comparer les valeurs de n'importe quel thread. L'exemple ci-après montre comment utiliser la méthode Increment pour incrémenter une variable partagée par des procédures exécutées sur des threads distincts.

Sub ThreadA(ByRef IntA As Integer)
 System.Threading.Interlocked.Increment(IntA)
End Sub

Sub ThreadB(ByRef IntA As Integer)
 System.Threading.Interlocked.Increment(IntA)
End Sub

Classe ReaderWriter Locks

Il est possible de ne verrouiller une ressource que pendant l'écriture des données et d'autoriser la lecture par plusieurs clients lorsqu'aucune mise à jour ne se déroule. La classe ReaderWriterLock impose l'accès exclusif à une ressource pendant qu'un thread la modifie, mais elle autorise l'accès non-exclusif pendant sa lecture. Le verrouillage de type ReaderWriter constitue une alternative pratique aux verrous exclusifs qui provoquent l'attente des autres threads, y compris de ceux qui n'ont pas de mises à jour à effectuer. L'exemple ci-après montre comment utiliser le verrouillage ReaderWriter pour coordonner les opérations de lecture et d'écriture par plusieurs threads.

Class ReadWrite
'Les méthodes ReadData et WriteData peuvent être appelées 
' en toute sécurité à partir de plusieurs threads.
 Public ReadWriteLock As New System.Threading.ReaderWriterLock()
 Sub ReadData()
 ' Cette procédure lit des informations depuis une source.
 ' Le verrouillage en lecture empêche l'écriture des données 
 ' tant que le thread n'a pas terminé la lecture, tout en permettant
 ' aux autres threads d'appeler ReadData.
 ReadWriteLock.AcquireReaderLock(System.Threading.Timeout.Infinite)
 Try
 ' Opération de lecture ici.
 Finally
 ReadWriteLock.ReleaseReaderLock() ' Libérer le verrouillage en lecture.
 End Try
 End Sub

 Sub WriteData()
 ' Cette procédure écrit des informations vers une source.
 ' Le verrouillage en écriture empêche la lecture ou l'écriture des 
 ' données tant que le thread n'a pas terminé l'écriture.
 ReadWriteLock.AcquireWriterLock(System.Threading.Timeout.Infinite)
 Try
 ' Opération d'écriture ici.
 Finally
 ReadWriteLock.ReleaseWriterLock() ' Libérer le verrouillage en écriture.
 End Try
 End Sub
End Class
Blocages

La synchronisation de threads est appréciable dans les applications multithread, mais il existe toujours un risque de blocages au cours desquels plusieurs threads s'attendent mutuellement. Les blocages interrompent toute activité, un peu comme à un carrefour comportant quatre "stops" où les voitures attendraient chacune que les autres passent. Bien évidemment, il est essentiel d'empêcher ces blocages. De nombreuses situations sont à l'origine de conditions de blocage et il existe autant de solutions pour les éviter. Bien que cet article n'ait pas pour but de passer en revue toutes les implications des blocages, il convient toutefois de souligner qu'une planification soignée va permettre de les éviter. Il est souvent possible de prévoir les situations de blocage en établissant un diagramme des applications multithread avant de commencer la phase de codage.

Minuterie de thread

La classe Threading.Timer est utile pour exécuter périodiquement une tâche sur un thread distinct. Par exemple, vous allez utiliser une minuterie de thread pour vérifier l'état et l'intégrité d'une base de données ou pour sauvegarder des fichiers critiques. Dans l'exemple ci-après, une tâche est déclenchée toutes les deux secondes et un indicateur lance la méthode Dispose qui arrête la minuterie. Cet exemple affiche l'état dans la fenêtre de résultat ; par conséquent, rendez-la visible en appuyant sur Control+Alt+O avant de tester le code.

Class StateObjClass
' Utilisé pour contenir les paramètres pour les appels à TimerTask
 Public SomeValue As Integer
 Public TimerReference As System.Threading.Timer
 Public TimerCanceled As Boolean
End Class

Sub RunTimer()
 Dim StateObj As New StateObjClass()
 StateObj.TimerCanceled = False
 StateObj.SomeValue = 1
 Dim TimerDelegate As New Threading.TimerCallback(AddressOf TimerTask)
 ' Créez une minuterie qui appelle une procédure toutes les 2 secondes.
 ' Remarque : il n'y a pas de méthode Start ; la minuterie se déclenche dès que 
 ' l'instance est créée.
 Dim TimerItem As New System.Threading.Timer(TimerDelegate, StateObj, _
 2000, 2000)
 StateObj.TimerReference = TimerItem ' Enregistrer une référence pour Dispose.

 While StateObj.SomeValue < 10 ' Exécution pendant dix boucles.
 System.Threading.Thread.Sleep(1000) ' Attendre une seconde.
 End While

 StateObj.TimerCanceled = True ' Appeler Dispose de l'objet timer.
End Sub

Sub TimerTask(ByVal StateObj As Object)
 Dim State As StateObjClass = CType(StateObj, StateObjClass)
 Dim x As Integer
 ' Utilisez la classe interlocked pour incrémenter la variable de compteur.
 System.Threading.Interlocked.Increment(State.SomeValue)
 Debug.WriteLine("Nouveau thread lancé " & Now)
 If State.TimerCanceled Then ' Méthode Dispose appelée.
 State.TimerReference.Dispose()
 Debug.WriteLine("Terminé " & Now)
 End If
End Sub

La minuterie de thread est particulièrement utile lorsque la classe System.Windows.Forms.Timer n'est pas disponible, par exemple lorsque vous développez des applications de console.

Annulation des tâches

L'un des avantages du traitement multithread est que la partie interface utilisateur d'une application continue de répondre, même en cas d'exécution de tâches sur d'autres threads. Les événements de synchronisation et les champs qui jouent le rôle d'indicateurs servent souvent à avertir un autre thread de la nécessité de s'interrompre. L'exemple ci-après utilise des événements de synchronisation pour annuler une tâche. Pour utiliser cet exemple, ajoutez le module suivant à un projet. Pour démarrer un thread, appelez la méthode StartCancel.StartTask(). Pour annuler un ou plusieurs threads en cours d'exécution, appelez la méthode StartCancel.CancelTask().

Module StartCancel
 Public CancelThread As New System.Threading.ManualResetEvent(False)
 Public ThreadisCanceled As New System.Threading.ManualResetEvent(False)
 Private Sub SomeLongTask()
 Dim LoopCount As Integer
 Dim Loops As Integer = 10
 ' Exécuter le code dans une boucle While jusqu'à ce que 10 secondes se soient écoulées 
 ' ou jusqu'à ce que CancelThread soit défini.
 While Not CancelThread.WaitOne(0, False) And LoopCount < Loops
 ' Exécution d'une tâche ici.
 System.Threading.Thread.Sleep(1000) ' En sommeil pendant une seconde.
 LoopCount += 1
 End While
 If CancelThread.WaitOne(0, False) Then
 'Accuser réception du fait que ManualResetEvent CancelThread est défini.
 ThreadisCanceled.Set()
 MsgBox("Annulation du thread")
 Else
 MsgBox("Le thread est terminé")
 End If
 End Sub

 Public Sub StartTask()
 ' Démarre un nouveau thread.
 Dim th As New System.Threading.Thread(AddressOf SomeLongTask)
 CancelThread.Reset()
 ThreadisCanceled.Reset()
 th.Start()
 MsgBox("Thread démarré")
 End Sub

 Public Sub CancelTask()
 ' Arrête tout thread démarré par la procédure StartTask.
 ' Remarquez que ce thread reçoit et envoie 
 ' des événements de synchronisation pour coordonner les threads. 
 CancelThread.Set() ' Définit CancelThread pour demander au thread de s'arrêter.
 If ThreadisCanceled.WaitOne(4000, False) Then
 ' Attendre 4 secondes que le thread 
 ' accuse réception de l'arrêt.
 MsgBox("Le thread s'est arrêté.")
 Else
 MsgBox("Le thread n'a pas pu être arrêté.")
 End If
 End Sub
End Module

Conclusions

Le traitement multithread constitue la clé des applications évolutives et réactives. Visual Basic .NET prend en charge un modèle de développement multithread robuste qui permet aux développeurs d'exploiter rapidement la puissance des applications multithread.

  • Visual Basic .NET utilise les nouvelles classes .NET Framework qui facilitent la création d'applications multithread.
  • Bien que l'utilisation de plusieurs threads puisse améliorer les performances, n'oubliez pas qu'à chaque thread est associé un coût en terme de mémoire supplémentaire nécessaire pour le créer et de temps processeur pour l'exécuter.
  • Les propriétés et les méthodes des threads contrôlent leur interaction et déterminent à quel moment les ressources sont disponibles pour leur exécution.
  • Malgré l'apparente confusion que semble introduire le traitement multithread, vous pouvez contrôler les threads en cours d'exécution grâce à des techniques de synchronisation.
  • Le traitement multithread autorise la mise en oeuvre d'applications évolutives en allouant efficacement les ressources disponibles, même lorsque la complexité d'une application s'accroît.

L'application des techniques décrites dans cet article permet de développer des applications professionnelles capables de gérer les tâches les plus consommatrices de temps processeur.



Dernière mise à jour le jeudi 23 mai 2002



Pour en savoir plus
Cela vous a-t-il été utile ?
(1500 caractères restants)
Merci pour vos suggestions.
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. Tous droits réservés.