# Programmation C sharp/Threads et synchronisation

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 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 System.Threading. La classe Thread gère un thread. Son constructeur accepte comme premier paramètre :
* soit un délégué de type ThreadStart :
* soit un délégué de type ParameterizedThreadStart :
Le second paramètre est optionnel : int maxStackSize. Il indique la taille maximale de la pile à allouer au nouveau thread.
Une fois le thread créé, il faut appeler la méthode Start pour démarrer le thread :
* soit Start si le delegate n'a aucun paramètre (ThreadStart),
* soit Start(object parameter) si le delegate accepte un paramètre de type object (ParameterizedThreadStart).
Exemple 1 :
Exemple 2 : Une méthode acceptant un paramètre.
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.

## 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 :
* void Join : attend indéfiniment que le thread se termine,
* bool Join(int millisecondsTimeout) : attend que le thread se termine dans le temps imparti spécifié en millisecondes. Cette méthode retourne true si le thread s'est terminé dans le temps imparti, et false sinon,
* bool Join(TimeSpan timeout) : cette méthode fonctionne de la même manière que la précédente, excepté que le temps imparti est spécifié par une structure de type System.TimeSpan.
Exemple :

## Suspendre et Arrêter un thread
### Suspendre un thread

Pour suspendre un thread, il faut appeler la méthode Suspend. Le thread est alors suspendu jusqu'à l'appel à la méthode Resume. 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 Interrupt. 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 try..finally, voire utiliser using).
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 :
Cependant, l'utilisation d'un bloc try..finally (ou du mot clé using) 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 Thread :

### bool IsAlive

    (lecture seule) Indique si le thread est toujours actif.

### bool IsBackground

    Indique si le thread est un thread d'arrière plan. Un thread d'arrière plan ne permet pas de maintenir l'exécution d'une application. C'est à dire qu'une application est terminée dès qu'il n'y a plus aucun thread de premier plan en cours d'exécution.

### bool IsThreadPoolThread

    (lecture seule) Indique si le thread appartient au pool de threads standard.

### int ManagedThreadId

    (lecture seule) Retourne l'identifiant attribué au thread par le framework .Net.

### string Name

    Nom attribué au thread.

### ThreadPriority Priority

    Priorité du thread :
   * Lowest : priorité la plus basse,
   * BelowNormal : priorité inférieure à normale,
   * Normal : priorité normale,
   * AboveNormal : priorité supérieure à normale,
   * Highest : priorité la plus haute.

### ThreadState ThreadState

    (lecture seule) État actuel du thread.
    Cette propriété est une combinaison de valeurs de l'énumération ThreadState :
   * Running = 0x00000000 : en cours d'exécution,
   * StopRequested = 0x00000001 : arrêt demandé,
   * SuspendRequested = 0x00000002 : suspension demandée,
   * Background = 0x00000004 : thread d'arrière plan,
   * Unstarted = 0x00000008 : thread non démarré,
   * Stopped = 0x00000010 : thread arrêté,
   * WaitSleepJoin = 0x00000020 : thread en attente (wait, sleep, ou join),
   * Suspended = 0x00000040 : thread suspendu,
   * AbortRequested = 0x00000080 : abandon demandé,
   * Aborted = 0x00000100 : thread abandonné.

## 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 ThreadPool. Par défaut, le pool contient 25 threads par processeur.
La méthode QueueUserWorkItem permet d'ajouter une nouvelle tâche :
Le premier paramètre est un délégué dont la signature est la suivante :
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 :

#### void Enter(object obj)

    Cette méthode suspend le thread appelant si un autre thread possède déjà l'accès exclusif, ou retourne immédiatement sinon.

#### void Exit(object obj)

    Cette méthode met fin à l'accès exclusif par le thread appelant, et permet à un thread suspendu d'obtenir l'accès exclusif à son tour.

#### bool TryEnter(object obj)

    Cette méthode permet de tenter d'obtenir l'accès exclusif. Elle retourne true si l'accès exclusif est obtenu, false sinon.

#### bool TryEnter(object obj,int milliseconds)

    Cette méthode permet de tenter d'obtenir l'accès exclusif, dans le temps imparti spécifié en millisecondes. Elle retourne true si l'accès exclusif est obtenu, false sinon.
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 :
Ce programme affiche :
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 try..finally :

### Le mot clé lock

Le mot clé lock est utilisable pour produire un code équivalent au précédent :
Le mot clé lock 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 WaitHandle. Les objets WaitHandle possèdent deux états :
* l'état signalé (set) dans lequel l'objet permet aux threads d'accéder à la ressource,
* l'état non signalé (reset) ne permet pas de nouvel accès. Toute tentative d'accès suspendra le thread, jusqu'au retour de l'état signalé.

#### Attendre l'état signalé d'un objet WaitHandle

Les méthodes suivantes de la classe WaitHandle permettent d'attendre l'état signalé d'un objet WaitHandle :
Il est possible d'appeler ces méthodes en spécifiant un temps limite (int: nombre de millisecondes ou TimeSpan) et un indicateur booléen exitContext. Ce paramètre vaut true si le contexte de synchronisation (l'ensemble des verrous) doit être libéré avant l'attente puis récupéré ensuite. Il vaut false sinon, c'est à dire que les verrous seront toujours détenus par le thread durant son attente.
Ces méthodes retournent true si l'état est signalé, et false sinon (temps limite expiré).

#### Attendre l'état signalé de plusieurs objets WaitHandle

Les méthodes statiques suivantes de la classe WaitHandle permettent d'attendre l'état signalé pour un tableau d'objets WaitHandle :
Le tableau d'objets WaitHandle ne doit pas comporter de doublon, sinon une exception DuplicateWaitObjectException est levée.
Il est possible d'appeler ces méthodes en spécifiant un temps limite (int: nombre de millisecondes ou TimeSpan) et un indicateur booléen exitContext (voir section précédente).
Les méthodes WaitAll retournent true si l'état de tous les objets est signalé, ou false sinon (temps limite expiré).
Les méthodes WaitAny retournent l'indice dans le tableau de l'objet dont l'état est signalé, ou la constante WaitHandle.WaitTimeout sinon (temps limite expiré).

#### Signaler et attendre

Les méthodes statiques suivantes permettent de mettre l'état signalé sur un objet WaitHandle et d'attendre un autre objet WaitHandle :

### Évènements

Un évènement est une instance de la classe EventWaitHandle (sous-classe de la classe WaitHandle vue précédemment). Il permet de modifier son état signalé / non-signalé grâce aux deux méthodes suivantes :
Cette classe possède le constructeur suivant :
Les paramètres sont les suivants :

#### initialState

    État initial : signalé (true) ou non-signalé (false).

#### mode

    Mode pour le retour à l'état non-signalé : AutoReset ou ManualReset :
   * ManualReset : le retour à l'état non-signalé (reset) se fait explicitement en appelant la méthode Reset.
   * AutoReset : le retour à l'état non-signalé est automatiquement effectué quand un thread est activé, c'est à dire quand il a terminé d'attendre l'état signalé avec une méthode wait (WaitOne, WaitAny ou WaitAll).
    Il y a une sous-classe pour chacun des deux modes (voir ci-dessous pour une description).

#### Synchronisation inter-processus

Cette classe possède également le constructeur suivant :
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 :

##### name

    Nom unique identifiant cette instance de la classe EventWaitHandle.

##### createdNew

    Référence à une variable booléenne que la fonction va utiliser pour indiquer si un nouvel objet EventWaitHandle a été créé (true) ou s'il existe déjà (false).

##### eventSecurity

    Définit les conditions d'accès à l'objet partagé.
Elle possède également une méthode statique permettant de retrouver une instance de la classe EventWaitHandle existante partagée au niveau système :
Le paramètre rights est optionnel et permet d'accéder à l'objet partagé.

#### Sous-classes

Cette classe a deux sous-classes :
* AutoResetEvent : Le retour à l'état non-signalé est automatique. Une fois que l'état signalé est obtenu par un thread (fin de l'attente par une méthode wait), l'objet AutoResetEvent revient à l'état non-signalé, empêchant un autre thread d'obtenir l'état signalé.
* ManualResetEvent : Le retour à l'état non-signalé se fait explicitement.
Ces deux sous-classes ont un constructeur qui accepte comme paramètre un booléen initialState indiquant son état initial : true pour l'état signalé, false pour l'état non-signalé.
Exemple :

### 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 WaitHandle 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é :
Comme pour une instance de la classe AutoResetEvent, le retour à l'état non-signalé est automatique, quand un thread sort de la fonction d'attente.
Cette classe possède le constructeur suivant :
Tous les paramètres sont optionnels :
* initiallyOwned : true si le mutex est dans l'état non-signalé et appartient au thread appelant, false sinon (état signalé).
* Les autres paramètres servent à la synchronisation inter-processus et sont décrits dans la section "Synchronisation inter-processus" précédente.
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.
* Sémaphore
* Mutex

## 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é volatile 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 :
* Référence d'objet ;
* Pointeur (Code non vérifié), toutefois l'objet pointé n'est pas volatile, seulement le pointeur lui-même, et il n'est pas possible de déclarer un pointeur vers volatile ;
* Types primitifs : sbyte, byte, short, ushort, int, uint, char, float, et bool ;
* Énumération avec un type primitif de base : byte, sbyte, short, ushort, int, ou uint ;
* Paramètres de types générique définis comme de type référence ;
* IntPtr et UIntPtr.
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 lock 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.