Un thread est un contexte d'exécution ayant sa propre pile de paramètres et de variables locales, mais partageant les mêmes variables globales (variables statiques des classes) que les autres threads du même processus (la même instance d'une application créée au moment de son lancement).
Initialement, un processus ne possède qu'un seul thread. En général, celui-ci crée d'autres threads pour le traitement asynchrone de la file de messages provenant du système d'exploitation (gestion des évènements), du matériel géré, ...
Les classes utilisées dans ce chapitre sont définies dans l'espace de nom codice_1 :
using System.Threading;
Créer et démarrer un thread.
Un thread est créé pour effectuer une tâche en parallèle d'une autre. Si l'application possède une interface graphique et doit effectuer une tâche qui prend du temps (calcul, téléchargement, ...), si un seul thread est utilisé, durant la longue tâche l'interface graphique est inutilisable. Il vaut mieux effectuer la tâche longue dans un nouveau thread afin que l'utilisateur puisse continuer à utiliser l'interface graphique (pour annuler cette tâche par exemple).
Les threads sont également nécessaires pour gérer plusieurs tâches indépendantes les unes des autres, comme par exemple pour un serveur, la gestion de connexions simultanées avec plusieurs clients. Dans ce cas, chaque thread effectue les mêmes tâches. Ils sont en général gérés par un ensemble de threads ("Thread pool" en anglais).
Les threads et les outils associés sont gérés par les classes de l'espace de nom codice_1.
La classe codice_3 gère un thread. Son constructeur accepte comme premier paramètre :
 delegate void ThreadStart()
 delegate void ParameterizedThreadStart(object parameter)
Le second paramètre est optionnel : codice_6.
Il indique la taille maximale de la pile à allouer au nouveau thread.
Une fois le thread créé, il faut appeler la méthode codice_7 pour démarrer le thread :
Exemple 1 :
// La tâche qui prend du temps ...
private void longTask()
 Console.WriteLine("Début de la longue tâche dans le thread "
 + Thread.CurrentThread.GetHashCode());
// Démarre un nouveau thread pour la méthode longTask()
public void StartLongTask()
 Console.WriteLine("Création d'un thread à partir du thread "
 + Thread.CurrentThread.GetHashCode());
 Thread th = new Thread(
 new ThreadStart( this.longTask )
 th.Start();
Exemple 2 : Une méthode acceptant un paramètre.
// La tâche qui prend du temps ...
private void telecharger(string url)
 Console.WriteLine("Début du téléchargement de " + url +
 " dans le thread " + Thread.CurrentThread.GetHashCode());
// Démarre un nouveau thread pour la méthode telecharger()
public void CommencerATelecharger(string url)
 Console.WriteLine("Création d'un thread à partir du thread "
 + Thread.CurrentThread.GetHashCode());
 Thread th = new Thread(
 new ParameterizedThreadStart( this.telecharger )
 th.Start( url );
Exemple 3 : Si la méthode doit accepter plusieurs paramètres, il faut passer un tableau de paramètres. Puisqu'un tableau est egalement un objet, le tableau peut être passé sous la forme d'une référence d'objet, qui sera reconverti en tableau dans la méthode du delegate.
// La tâche qui prend du temps ...
private void telecharger(object object_parameters)
 object[] parameters = (object[]) object_parameters;
 string url = (string) parameters[0];
 int tentatives = (int) parameters[1];
 Console.WriteLine("Début du téléchargement de " + url +
 " ("+tentatives+" tentatives) dans le thread " +
 Thread.CurrentThread.GetHashCode());
// Démarre un nouveau thread pour la méthode telecharger()
public void CommencerATelecharger(string url, int tentatives)
 Console.WriteLine("Création d'un thread à partir du thread "
 + Thread.CurrentThread.GetHashCode());
 Thread th = new Thread(
 new ParameterizedThreadStart( this.telecharger )
 th.Start( new object[] { url, tentatives } );
Attendre la fin d'un thread.
Une fois qu'un thread a démarré, la méthode join peut être appelée pour attendre que la méthode du thread se termine.
Cette méthode est surchargée :
Exemple :
// Démarre un nouveau thread pour la méthode longTask()
public void startLongTask()
 Console.Writeline("Création d'un thread à partir du thread "
 + Thread.CurrentThread.GetHashCode());
 Thread th = new Thread(
 new ThreadStart( this.longTask )
 Console.Writeline("Démarrage du thread " + th.GetHashCode() );
 th.Start();
 Console.Writeline("Attente de la fin du thread " + th.GetHashCode()
 + " pendant 5 secondes ...");
 bool fini = th.Join(5000); // 5000 ms
 if (!fini) // si pas fini
 Console.Writeline("Le thread " + th.GetHashCode()
 + " n'a pas terminé au bout de 5 secondes, attente indéfinie ...");
 th.Join();
 Console.Writeline("Le thread " + th.GetHashCode() + " a terminé sa tâche.");
Suspendre et Arrêter un thread.
Suspendre un thread.
Pour suspendre un thread, il faut appeler la méthode codice_21. Le thread est alors suspendu jusqu'à l'appel à la méthode codice_22. Cependant ces deux méthodes sont obsolètes, car un thread suspendu détient toujours les ressources qu'il avait acquis avant sa suspension. Ce problème risque de provoquer le blocage d'autres threads tant que celui-là est suspendu.
Arrêter un thread.
L'arrêt d'un thread peut être demandé en appelant la méthode codice_23. Cette méthode provoque le lancement d'une exception lorsque le thread appelera une méthode d'entrée-sortie. Donc l'arrêt du thread n'est pas instantané.
Cependant l'appel à cette méthode peut interrompre le thread à n'importe quel moment de son exécution. Il faut donc prévoir que ce genre d'exception soit lancé pour pouvoir libérer les ressources dans un bloc codice_24, voire utiliser codice_25).
Une solution alternative est de tester une condition de terminaison du thread. Ceci permet de spécifier où le thread peut se terminer, et libérer les ressources correctement.
Exemple :
public void UnThread()
 // ... initialisation
 while ( continuer_thread )
 // ... tâches du thread
 // ... libération des ressources
Cependant, l'utilisation d'un bloc codice_24 (ou du mot clé codice_25) est tout de même nécessaire au cas où le système interromperait le thread (fin brutale de l'application par exemple).
Propriétés d'un thread.
Voici une liste des principales propriétés de la classe codice_3 :
Le pool de threads.
Un pool de threads est associé à chaque processus. Il est composé de plusieurs threads réutilisables effectuant les tâches qu'on lui assigne dans une file d'attente. Une tâche est placée dans la file d'attente, puis on lui affecte un thread inocupé qui l'effectuera, puis le thread se remet en attente d'une autre tâche. Une partie de ces threads est consacrée aux opérations d'entrées-sorties asynchrones.
Le pool de threads est géré par les méthodes statiques de la classe codice_52. Par défaut, le pool contient 25 threads par processeur.
La méthode QueueUserWorkItem permet d'ajouter une nouvelle tâche :
 bool QueueUserWorkItem ( WaitCallback callback, object state )
Le premier paramètre est un délégué dont la signature est la suivante :
 delegate void WaitCallback(object state)
Le second paramètre est optionnel et contient l'argument passé au délégué.
Le délégué passé en paramètre effectuera la tâche, dans le thread qui lui sera attribué.
Synchronisation entre les threads.
Le fait que tous les threads d'un même processus partagent les mêmes données signifie que les threads peuvent accéder simultanément à un même objet. Si un thread modifie un objet (écriture, en utilisant une méthode de l'objet par exemple) pendant qu'un autre thread récupère des informations sur cet objet (lecture), ce dernier peut obtenir des données incohérentes résultant d'un état intermédiaire temporaire de l'objet accédé.
Pour résoudre ce genre de problème, des outils de synchronisation permettent de suspendre les threads essayant d'accéder à un objet en cours de modification par un autre thread.
Moniteur.
Un moniteur ("monitor" en anglais) ne permet l'accès qu'à un seul thread à la fois. C'est à dire que si plusieurs threads essaye d'accéder au même moniteur, un seul obtiendra l'accès, les autres étant suspendus jusqu'à ce qu'ils puissent à leur tour détenir l'accès exclusif.
La classe Monitor gère ce type d'objet de synchronisation. Toutes les méthodes de cette classe sont statiques. Les principales méthodes sont :
L'objet passé en paramètre identifie le moniteur accédé. C'est à dire que tout objet peut être utilisé comme moniteur. En C#, tout est objet, même les chaînes de caractères, et les valeurs numériques et booléennes. Cependant, il n'est pas recommandé d'utiliser de telles valeurs ou des références publiques, car ce sont des références globales. Il est préférable d'utiliser des membres privés, voire des variables locales.
Exemple :
using System;
using System.Threading;
public class TestMonitor
 private object synchro = new object();
 public void MethodeThread()
 int id = Thread.CurrentThread.ManagedThreadId;
 Console.WriteLine("Début du thread " + id );
 Monitor.Enter( synchro );
 Console.WriteLine("Le thread " + id + " entre exclusivement ..." );
 Thread.Sleep(1000); // attend 1 seconde
 Console.WriteLine("Le thread " + id + " sort ..." );
 Monitor.Exit( synchro );
 Console.WriteLine("Fin du thread " + id );
 public void Test()
 // 2 threads
 Thread
 thread1 = new Thread( new ThreadStart( MethodeThread ) ),
 thread2 = new Thread( new ThreadStart( MethodeThread ) );
 thread1.Start();
 thread2.Start();
 public static void Main()
 new TestMonitor().Test();
Ce programme affiche :
 Début du thread 3
 Le thread 3 entre exclusivement ...
 Début du thread 4
 Le thread 3 sort ...
 Le thread 4 entre exclusivement ...
 Fin du thread 3
 Le thread 4 sort ...
 Fin du thread 4
Ce programme ne tient pas compte des exceptions. Il faut cependant les prévoir pour s'assurer de libérer le moniteur, en utilisant un bloc codice_24 :
 public void MethodeThread()
 int id = Thread.CurrentThread.ManagedThreadId;
 Console.WriteLine("Début du thread " + id );
 Monitor.Enter( synchro );
 try
 Console.WriteLine("Le thread " + id + " entre exclusivement ..." );
 Thread.Sleep(1000); // attend 1 seconde
 Console.WriteLine("Le thread " + id + " sort ..." );
 finally
 Monitor.Exit( synchro );
 Console.WriteLine("Fin du thread " + id );
Le mot clé codice_58.
Le mot clé codice_58 est utilisable pour produire un code équivalent au précédent :
 public void MethodeThread()
 int id = Thread.CurrentThread.ManagedThreadId;
 Console.WriteLine("Début du thread " + id );
 lock( synchro ) // <- enter ( synchro )
 Console.WriteLine("Le thread " + id + " entre exclusivement ..." );
 Thread.Sleep(1000); // attend 1 seconde
 Console.WriteLine("Le thread " + id + " sort ..." );
 } // <- exit ( synchro )
 Console.WriteLine("Fin du thread " + id );
Le mot clé codice_58 est suivi de la référence de l'objet dont l'accès doit être exclusif durant le bloc de code qui suit.
Autres outils de synchronisation.
Les autres outils de synchronisation dérivent de la classe abstraite codice_61.
Les objets codice_61 possèdent deux états :
Attendre l'état signalé d'un objet codice_61.
Les méthodes suivantes de la classe codice_61 permettent d'attendre l'état signalé d'un objet codice_61 :
// Attendre que l'objet WaitHandle soit dans l'état signalé :
public virtual bool WaitOne();
public virtual bool WaitOne(int millisecondsTimeout, bool exitContext);
public virtual bool WaitOne(TimeSpan timeout, bool exitContext);
Il est possible d'appeler ces méthodes en spécifiant un temps limite (codice_66: nombre de millisecondes ou codice_67) et un indicateur booléen codice_68. Ce paramètre vaut codice_17 si le contexte de synchronisation (l'ensemble des verrous) doit être libéré avant l'attente puis récupéré ensuite. Il vaut codice_18 sinon, c'est à dire que les verrous seront toujours détenus par le thread durant son attente.
Ces méthodes retournent codice_17 si l'état est signalé, et codice_18 sinon (temps limite expiré).
Attendre l'état signalé de plusieurs objets codice_61.
Les méthodes statiques suivantes de la classe codice_61 permettent d'attendre l'état signalé pour un tableau d'objets codice_61 :
// Attendre que tous les objets WaitHandle soit dans l'état signalé :
public static bool WaitAll(WaitHandle[] waitHandles);
public static bool WaitAll(WaitHandle[] waitHandles,
 int millisecondsTimeout, bool exitContext);
public static bool WaitAll(WaitHandle[] waitHandles,
 TimeSpan timeout, bool exitContext);
// Attendre qu'au moins l'un des objets WaitHandle soit dans l'état signalé :
public static int WaitAny(WaitHandle[] waitHandles);
public static int WaitAny(WaitHandle[] waitHandles,
 int millisecondsTimeout, bool exitContext);
public static int WaitAny(WaitHandle[] waitHandles,
 TimeSpan timeout, bool exitContext);
Le tableau d'objets codice_61 ne doit pas comporter de doublon, sinon une exception codice_77 est levée.
Il est possible d'appeler ces méthodes en spécifiant un temps limite (codice_66: nombre de millisecondes ou codice_67) et un indicateur booléen codice_68 (voir section précédente).
Les méthodes codice_81 retournent codice_17 si l'état de tous les objets est signalé, ou codice_18 sinon (temps limite expiré).
Les méthodes codice_84 retournent l'indice dans le tableau de l'objet dont l'état est signalé, ou la constante codice_85 sinon (temps limite expiré).
Signaler et attendre.
Les méthodes statiques suivantes permettent de mettre l'état signalé sur un objet codice_61 et d'attendre un autre objet codice_61 :
// Mettre l'état signalé sur l'objet toSignal
// Ensuite, attendre que l'objet toWaitOn soit dans l'état signalé :
public static bool SignalAndWait(WaitHandle toSignal, WaitHandle toWaitOn);
public static bool SignalAndWait(WaitHandle toSignal, WaitHandle toWaitOn,
 int millisecondsTimeout, bool exitContext);
public static bool SignalAndWait(WaitHandle toSignal, WaitHandle toWaitOn,
 TimeSpan timeout, bool exitContext);
Évènements.
Un évènement est une instance de la classe codice_88 (sous-classe de la classe codice_61 vue précédemment).
Il permet de modifier son état signalé / non-signalé grâce aux deux méthodes suivantes :
public bool Reset(); // -> état non-signalé
public bool Set(); // -> état signalé
Cette classe possède le constructeur suivant :
public EventWaitHandle(bool initialState, EventResetMode mode);
Les paramètres sont les suivants :
Synchronisation inter-processus.
Cette classe possède également le constructeur suivant :
public EventWaitHandle(bool initialState, EventResetMode mode,
 string name, ref Boolean createdNew,
 System.Security.AccessControl.EventWaitHandleSecurity eventSecurity);
Les trois paramètres supplémentaires sont tous optionnels et sont utilisés pour le partage au niveau système et donc pour la synchronisation entre processus :
Elle possède également une méthode statique permettant de retrouver une instance de la classe codice_88 existante partagée au niveau système :
public static EventWaitHandle OpenExisting(string name, System.Security.AccessControl.EventRights rights);
Le paramètre codice_98 est optionnel et permet d'accéder à l'objet partagé.
Sous-classes.
Cette classe a deux sous-classes :
Ces deux sous-classes ont un constructeur qui accepte comme paramètre un booléen codice_102 indiquant son état initial : codice_17 pour l'état signalé, codice_18 pour l'état non-signalé.
Exemple :
using System;
using System.Threading;
class Exemple
 static AutoResetEvent evenementTermine;
 static void AutreThread()
 Console.WriteLine(" Autre thread : 0% accompli, attente...");
 evenementTermine.WaitOne(); // Attend état signalé + Reset (auto)
 // l'état a été signalé, mais retour à l'état non-signalé
 // maintenant que l'appel à WaitOne est terminé.
 Console.WriteLine(" Autre thread : 50% accompli, attente...");
 evenementTermine.WaitOne(); // Attend état signalé + Reset (auto)
 // l'état a été signalé, mais retour à l'état non-signalé
 // maintenant que l'appel à WaitOne est terminé.
 Console.WriteLine(" Autre thread : 100% accompli, terminé.");
 static void Main()
 evenementTermine = new AutoResetEvent(false);
 Console.WriteLine("Main: démarrage de l'autre thread...");
 Thread t = new Thread(AutreThread);
 t.Start();
 Console.WriteLine("Main: tâche 1/2 : 1 seconde ...");
 Thread.Sleep(1000);
 evenementTermine.Set(); // -> état signalé
 Console.WriteLine("Main: tâche 2/2 : 2 secondes ...");
 Thread.Sleep(2000);
 evenementTermine.Set(); // -> état signalé
 Console.WriteLine("Main: fin des tâches.");
Verrou d'exclusion mutuelle.
Un verrou d'exclusion mutuelle (mutex en abrégé) permet de donner un accès exclusif pour une ressource à un seul thread à la fois.
Cette classe dérive de la classe codice_61 décrite avant.
Pour obtenir l'accès, il faut appeler une méthode wait (WaitOne par exemple), c'est à dire attendre l'état signalé. La méthode suivante permet de libérer le verrou en positionnant l'état signalé :
public void ReleaseMutex(); // -> État signalé
Comme pour une instance de la classe codice_99, le retour à l'état non-signalé est automatique, quand un thread sort de la fonction d'attente.
Cette classe possède le constructeur suivant :
public Mutex(bool initiallyOwned,
 string name, ref Boolean createdNew,
 System.Security.AccessControl.MutexSecurity mutexSecurity);
Tous les paramètres sont optionnels :
Cet objet est dédié à la synchronisation inter-processus.
Pour de l'exclusion mutuelle entre threads du même processus, du point de vue des performances, il est préférable d'utiliser un moniteur, ou l'instruction lock.
Attribut volatile.
Pour des raisons de performance, le système d'exécution peut décider de changer l'ordre des lectures et écritures en mémoire.
Cela peut gêner le fonctionnement quand un attribut de classe est accédé par différents threads en même temps.
Le mot-clé codice_110 résoud le problème car les champs déclarés comme volatiles ne sont pas soumis à ce changement, ce qui permet d'assurer que l'écriture par un thread et la lecture par un autre se fasse dans le bon ordre.
Le mot-clé peut être appliqué aux attributs d'une classe ou d'une structure des types suivants :
Les autres types (incluant double et long) ne peuvent être déclarés comme volatiles car les lectures et écritures ne sont pas faites de manière atomique, c'est à dire que l'accès (lecture/écriture) ne se fait pas en une fois, autorisant un autre thread à s'exécuter au milieu de l'opération.
Pour ces types, il faut utiliser les outils de synchronisation tels que codice_58 par exemple.
Le mot-clé ne s'applique pas aux variables locales car elles ne sont pas partagées avec d'autres threads, mais allouées sur la pile dédiée au thread.