18. Thread : programmation simultanée

Le but de cette leçon est de fournir les outils essentiels pour implémenter une application multithread en C#. Les threads sont des sous-processus pouvant être exécutés en parallèle ou en série. Ils travaillent ensemble pour atteindre un objectif commun, et, pour cette raison, ils peuvent également partager les mêmes ressources (variables, fichiers, etc.). Le but d’utiliser une application multithread est de tirer les processeurs / cœurs logés à l’intérieur de nos machines (PC ou Smartphone). Cela signifie que nos applications peuvent effectuer de nombreuses tâches en même temps, ce qui entraîne une augmentation de la vitesse de calcul.

Par exemple, pour les applications avec une interface graphique, vous pouvez penser à réaliser des éléments très complexes de code en background, tandis que les contrôles d’interface restent actifs. De même, pour une application serveur, l’utilisation de plusieurs threads vous permettra de gérer les connexions entrantes séparément. La documentation officielle sur le multithreading pour C# peut être trouvée à ce lien.

La classe Thread

En C#, un thread est représenté par la classe Thread, contenue dans le namespace System.Threading. Pour l’importer, ajoutons la clause using suivante en haut du code :

 
 
  1. using System.Threading;

 

Pour initialiser un thread, il suffit d’instancier la classe ThreadStart en ayant comme paramètre le nom de la méthode à exécuter en tant que thread :

 
 
  1. Thread t = new Thread(new ThreadStart(mythreadmethod));

 

La ligne précédente instancie un thread dont le nom est t, dont le code à exécuter est celui contenu dans myThreadMethod.

Si myThreadMethod est paramétré, nous suggérons fortement d’utiliser la technique suivante pour passer des paramètres :

 
 
  1. contiendra les paramètres à transmettre de input au thread
  2. ... // code pour instancier myParams
  3. Thread t = new Thread(new ThreadStart(myThreadMethod(myParams)));

 

En conséquence, myThreadMethod devra lancer l’objet myParams afin de dériver les paramètres nécessaires à l’exécution :

 
 
  1. void myThreadMethod(object myparams)
  2. {
  3. ... // code pour exécuter le casting de myparams et lire son contenu
  4. }

 

Les threads peuvent être exécutés en arrière-plan (background) et en foreground. La seule différence est que les threads d’arrière-plan sont immédiatement terminés lorsqu’il n’y a plus de threads en cours d’exécution en premier plan. Par défaut, le processus parent (qui est également un thread) est toujours exécuté au premier plan.

Les principales méthodes de la classe de threads sont les suivantes:

  • Start() : démarre l’exécution du thread. A partir du moment où cette méthode est invoquée, le processus appelant et le thread appelé exécuteront leurs lignes de code indépendamment l’une de l’autre, et éventuellement simultanément ;
  • Thread.Sleep (Int32 millis) : suspend l’exécution du thread pour le nombre de millisecondes indiqué ;
  • Finalize() : assure que toutes les ressources utilisées par le thread seront libérées du garbage collector ;
  • Abort() : termine le thread et renvoie une exception ThreadAbortException qui doit être gérée par le processus parent ;
  • Join() : bloque l’exécution du thread appelant jusqu’à ce que le thread pour lequel la méthode Join est appelée soit terminé.

Les propriétés les plus saillantes de la classe Thread sont :

  • isAlive : Booléen indiquant l’état d’exécution du thread ;
  • isBackground : indique si le thread s’exécute en arrière-plan ;
  • ThreadState : utile uniquement dans les scénarios de débogage, il surveille l’état d’exécution du thread.

 

Synchronisation entre les threads

L’exécution du thread ne se produit jamais dans un ordre préétabli. Cela signifie que plusieurs threads peuvent essayer d’accéder à la même ressource en même temps et qu’il n’y a aucune garantie qu’un thread se termine avant un autre. En général, nous essayons d’éviter de partager les ressources autant que possible, mais parfois nous ne pouvons pas nous en passer. Si vous souhaitez exécuter plusieurs threads avec une certaine forme d’organisation, en protégeant les ressources partagées, vous devrez recourir à la synchronisation.

L’un des principaux problèmes qui peuvent survenir lors de la synchronisation de différents threads est le deadlock (blocage). Il s’agit d’une situation dans laquelle deux ou plusieurs processus attendent mutuellement qu’un autre thread fasse quelque chose ; clairement, cela implique une impasse. La seule solution pour éviter les situations de blocage est de créer des diagrammes de multithreading avant de commencer le codage.

C# fournit au programmeur divers mécanismes pour assurer la synchronisation des threads. Le mot clé lock() est utilisé pour indiquer les parties de code qui doivent être exécutées sans interruption à partir d’autres threads. Le code inclus à l’intérieur est exécuté avec une exclusion mutuelle entre les différents threads.

La syntaxe est assez simple :

 
 
  1. lock (object) {
  2. ... // code protégé
  3. }

 

Ce morceau de code indique que tous les threads partageant une référence à l’objet objet doivent s’arrêter et attendre l’exécution du code thread en cours.


Événements de synchronisation

Parfois, il est utile pour un thread de bloquer son exécution et d’attendre qu’un événement se produise. Pour cela, C# a introduit les événements de synchronisation. Un événement de synchronisation est un objet qui peut prendre deux états : signalé et non signalé. Les classes pour les événements de synchronisation en C# sont AutoResetEvent et ManualResetEvent, à la différence qu’AutoResetEvent bascule son statut de signalé à non enregistré chaque fois qu’un thread est réactivé après avoir vérifié son état signalé.

Alors que ManualResetEvent doit être manuellement ramené à un état non signalé, en appelant la méthode Reset(). Pour instancier l’objet, il suffit de l’indiquer comme suit :

 
 
  1. AutoResetEvent autoEvent = new AutoResetEvent(false);

 

Les méthodes saillantes sont :

  • WaitOne() : attend jusqu’à ce que l’événement de synchronisation soit signalé. Lorsque l’événement de synchronisation est signalé, il sera immédiatement signalé à la condition de non signalé ;
  • WaitAny(WaitHandle []) et WaitAll (WaitHandle []) : vous permet d’attendre au moins un ou tous les événements de synchronisation contenus dans le tableau WaitHandle ;
  • Set() : change le statut de non signalé à signalé ;
  • Reset() : uniquement pour ManualResetEvent, modifie le statut de rapporté à non signalé.

 

Mutex

L’une des méthodes les plus classiques pour accorder un accès exclusif à une portion de code est l’utilisation d’une variable de type Mutex. Contrairement à Lock, qui limite sa portée au sein de l’application, un objet Mutex peut être utilisé pour protéger les ressources au niveau du système d’exploitation. Mutex est donc un mécanisme de verrouillage plus puissant et ne devrait être utilisé que pour la synchronisation entre des threads appartenant à différentes applications.

Déclarons comme il suit :

 
 
  1. private Mutex mut = new Mutex();

 

Pour vérifier son état, utilisez la méthode WaitOne() (comme pour les événements de synchronisation) et le thread appelant est bloqué jusqu’à ce que la variable soit débloquée, en appelant la méthode ReleaseMutex(). Pour éviter les situations de blocage, vous pouvez définir un délai après lequel le thread appelant renoncera à acquérir le contrôle du Mutex :

 
 
  1. if (mut.WaitOne(1000)) {
  2. // code
  3. }
Comments
Chargement ...
"