20 Juil 2016

Multithreading, locks et deadlocks en C#

Lorsque vous travaillez sur une application “multi-threadée”, il est fort probable que les différents threads partagent des données communes.
Pour éviter que plusieurs threads accèdent à une même donnée au même moment, le framework met à notre disposition “lock” et “Monitor”.

lock

“lock” est un mot clé en C# qui sécurise une portion de code considéré comme critique en la rendant exécutable par un seul thread à la fois. C’est un verrou qui se pose sur un objet et dès qu’un thread prend cet objet il est le seul à pouvoir exécuter le bloc de code juste après. Si un autre thread souhaite accéder à cette ressource, il sera mis en attente le temps que le premier thread soit sorti de ce bloc.

Voici un exemple illustrant cette pose de verrou :

Dans cet exemple, l’objet “monVerrou” est l’objet sur lequel un thread pose un verrou. Ceci rend le bloc de code entre “{” et “}”  (this.Montant += somme) inaccessible à d’autres threads tant que celui-ci ne sera pas ressorti de ce bloc.

Voici maintenant un exemple de mise en oeuvre :

Sorties possibles :

Capture Capture1

La pose du verrou assure l’intégrité du montant final sur mon compte, peu importe dans quel ordre les ordres de transactions s’effectue. Ici, 3 threads se lancent en même temps et crédite mon compte d’un montant différent. S’il n’y avait pas de verrou, le montant final ne serait pas assuré.

Deadlock

L’utilisation du mot clé “lock” peut être dangereux car il y a des risques de deadlock. Un deadlock se produit quand 2 threads s’attendent mutuellement et restent bloqués définitivement. Il faut donc faire attention à ce qu’on fait lorsque l’on manipule les verrous.

Exemple de mise en oeuvre :

Dans cet exemple, nous avons 2 threads lancés au même moment. Le premier simule un crédit sur mon compte et le deuxième un débit. Les 2 font appel à 2 méthodes distinctes de ma classe mais ces 2 méthodes posent un verrou sur les objets “monVerrou” et “monDeuxiemeVerrou” mais pas dans le même ordre. Un deadlock se produit :

  • le thread traitant le crédit pose un verrou sur “monVerrou“, fait un traitement de 100ms et attend que l’objet “monDeuxiemeVerrou” soit libéré
  • le thread traitant le débit s’exécute en parallèle et pose un verrou sur “monDeuxiemeVerrou“, fait un traitement de 100ms et attend que le l’objet “monVerrou” soit libéré.

Nous sommes en situation de deadlock. Les 2 threads s’attendent mutuellement et ne se termineront pas. Pour éviter ce genre de désagrément, il faut veiller à acquérir les verrous toujours dans le même ordre ou alors utiliser une classe mise à disposition par le framework : Monitor

Monitor

Cette classe statique se trouve dans le namespace “System.Threading”. A l’image de “lock“, elle permet de poser un verrou sur une ressource et ainsi interdire l’accès à ce même bout de code à tout autre processus. Elle fournit cependant des méthodes pour synchroniser l’accès à un bloc de code à protéger en posant ou en retirant un verrou :

  • Monitor.Enter => pose le verrou
  • Monitor.TryEnter => tente de poser un verrou
  • Monitor.Exit => retire le verrou

Sortie possible :

Capture

Une fois le verrou posé (Monitor.Enter(this.monVerrou);), le bloc à sécuriser se place dans le try et le retrait du verrou dans le finally (Monitor.Exit(this.monVerrou);).

Vous allez me dire… oui mais “lock” fait pareil !… Jusqu’ici, rien de neuf en effet, cependant Monitor fournit la méthode TryEnter permettant de passer un paramètre supplémentaire “timeout” (ou timespan) et retourne un booléen. Ce paramètre est un entier représentant un temps en ms et sert à faire attendre le thread souhaitant accéder à la ressource pendant ce laps de temps. Si ce temps est dépassé, la méthode retournera false, si par contre le verrou est libéré avant, la méthode retournera true.

Exemple :

Sortie possible :

Capture

Il ne faut pas oublier le Monitor.Exit dans le finally. De cette façon, là où une situation de deadlock pourrait se produire, l’utilisation d’un timeout permet de libérer les verrous et ainsi ne pas bloquer l’application (et ça c’est cool).

Exemple :

Sortie possible :

Capture

Prochainement, nous irons plus loin et je ferais un post sur les autres méthodes de Monitor (Wait, Pulse, PulseAll…).

Share