jeudi 14 octobre 2010

Sync C# - Méthodes de synchronisation

Introduction
Cet article fait suite à une l'article "Theading en C# - synchronisation et méthodes de threading" largement basé sur les publications de Joseph Albahari (auteur de C# in a nutshell) qui met à disposition une documentation très complete.
Que cela soit sur son site internet ou le document pdf, la lecture des 80 pages vaut largement le détour.

Dans mon précédent article, je me suis principalement intéressé au threading en mettant de côté toute la section synchronisation.
Par la suite, j'ai ajouté de nombreux exemples concernant le threading et laissant évidement un grand vide pour tout ce qui des exemples relatifs à la synchronisation.
C'est pourquoi l'article présent est né et sera complété au fur et à mesure.
Cet article ne sera pas un tutoriel approfondit (voir plutôt le site de Joseph Albahari) mais il reprendra un récapitulatif des différentes méthodes de synchronisations avec des liens vers divers exemples.

Tableau récapitulatif

Type de synchronisationdescriptionInter-ProcessusRapidité
lock Locking: Assure qu'un seul thread peut accéder une section de code. NON rapide
Mutex Locking: Assure qu'un seul thread peut accéder une ressource ou section de code. oui modéré
Semaphore Locking: Assure qu'un nombre maximum de thread puissent avoir accès a une ressource ou section de code oui modéré
EventWaitHandle Signalisation: permet à un thread d'attendre jusqu'a ce qu'il recoive un signal oui modéré
Wait And Pulse Signalisation: permet à un thread d'attendre jusqu'à jusqu'au moment ou la condition de blockage est recontrée. Pattern qui évite les threads d'être déschédulé. NON modéré
Sleep Bloque le thread (il est déschédulé) pendant une certain laps de temps n/a n/a
join Permet d'attendre qu'un autre thread termine son exécution. n/a n/a
Interlocked Librairie permettant de faire des opérations atomiques sans bloquer le thread (sans déschéduler le thread) NON 1* très rapide
volatile Permet un accès protégé a une variable sans utiliser de méthode de locking. Utilise des memory barrier mais peut réserver des suprises. A n'utiliser qu'en connaissance de cause. NON 1* très rapide
n/a: non applicable
1*: Cross-processus si on utilise de la mémoire partagée (shared mem).
Locking: Signale une méthode permettant de définir une section critique (comme l'instruction lock).
Signalisation: Signale une méthode permettant d'envoyer un signal.

Les différentes méthodes de synchronisation
 
EventWaitHandle, ManualResetEvent et AutoResetEvent
Un EventWaitHandle est un mécanisme permettant à un processus de signaler (setter) un évènement à l'intention d'un autre processus (listener).
Ce processus de synchronisation est l'un des plus utilisés et l'un des plus importants car il sert à construire d'autre procédés de synchronisation.
Dans le monde Unix, ce procédé de communication est couramment dénommé signal, ce qui à mon sens est plus parlant puisqu'un signal est envoyé dans le système (à l'intention de ceux qui désirent être avertis).
Le grand avantage de ce procédé de communication est que si l'on utilise des EventWaitHandle nommés (identifié par une string), il est alors possible de signaler l'évènement en cross-process.
Les implémentations les plus connues de EventWaitHandle sont les ManualResetEvent et AutoResetEvent.
AutoResetEvent :
L'AutoResetEvent doit être vu comme un portillon d'accès de métro.
Quand on insère le ticket, il est possible à une seule personne de passer le portillon... il se referme juste après.
Le processus qui met le ticket est celui qui fait l'opération "set" (le setter).
Le processus qui attend de passer le portillon est celui qui fait l'opération d'attente "WaitOne" (le listener).

La différence entre le ManuelResetEvent et l'AutoResetEvent, c'est que lorsque l'on fait un set sur un ManuelResetEvent, le portillon reste toute porte ouvertes (jusqu'à l'appel du Reset).

WaitHandle :
Il peut aussi être utile de préciser que la classe EventWaitHandle à pour ancêtre WaitHandle.
Cette même classe WaitHandle qui est également l'ancêtre des classes Semaphore et Mutex, autres mécanismes de synchronisation très importants.


Ressources:
Mutexes
Mutex signifie Mutuellement Exclusif.
C'est par exemple le cas d'un WC. Si quelqu'un est occupé dans la toilette, cette dernière n'est pas disponible pour une autre personne (sont usage est mutuellement exclusif). Le verrou est fermé lorsque la personne entre dans le WC et ouvert lorsqu'elle a terminée. Le verrou est le "mutex" protégeant une ressource non partageable (le wc) pendant son utilisation/manipulation.
Les autres personnes désirant utiliser le WC font alors la file et attendent patiemment que l'occupant libère les lieux (le verrou).


Plus informatique-ment parlant... A un moment quelconque du fonctionnement du logiciel (ou des logiciels puisqu'un mutex est cross-process), le mutex ne peut être détenu que par un et un seul processus à la fois. Les autres processus désirant acquérir le mutex doivent alors se montrer patient et attendre qu'il soit libéré.
Un mutex est donc bien pratique pour indiquer qu'une ressource non partageable est en cours d'utilisation.

Ressources:
  • Sync C# - Mutex
    Définition plus précise, exemple de mutex cross-process et exemple partage de reference de mutex.

Sémaphore
Un sémaphore c'est un peu comme une boite de nuit. Elle a une certaine capacité et est gardée par des videurs.
Lorsque la boite de nuit est remplie, les videurs ne laisse plus entrer personne et une file se forme dehors.
Lorsque des personnes sortent, le un nombre identique de personnes peuvent entrer (ce qui diminue la file d'attente).
Cette métaphore représente parfaitement le fonctionnement d'un sémaphore.

Ressources:
    Synchronisation Multi Reader Exclusive
    Ce type de synchronisation  particulière est prise en charge par les classes ReadWriteLock et ReadWriteLockSlim.
    ReadWriteLockSlim est un objet de synchronisation autorisant de multiples accès concurrents en lecture et un accès exclusif lors d'opération d'écriture.
    Pour décrire le fonctionnement gros, lorsque l'objet de synchronisation doit passer en accès d'écriture, il bloque tous les accès en lectures entrants... attend la fin de tous les accès de lecture en cours et accède par la suite en accès exclusif pour une opération d'écriture (de modification).
    Lorsque le ReadWriteLock est en mode d'écriture, tous les accès concurrents en lectures (tout comme un autre accès concurrentiel en écriture) sont mis en attente jusqu'au moment où l'on quitte le mode d'écriture.

    Ce paradigme est encore assez évident lorsqu'il est appliqué à la manipulation de fichier (plusieurs processus de lectures et un seule processus autorisé en écriture).
    Il l'est tout autant s'il est appliqué aux zones mémoires (référence vers des objets) accédés par différents threads d'une même application :-)

    Ressources:
    Thread Local Storage
    TLS pour les intimes, Thread Local Storage est une méthode permettant de stocker des informations au niveau du thread (dans un slot) en s'assurant qu'il existe un slot par Thread.
    Le slot peut être nommé (ou non). Il doit être déclaré hors du thread (avant qu'il ne démarre) mais doit impérativement être initialisé dans le thread (car chaque dispose de sa propre copie du slot).

    Petite note pour signaler que si un TLS nommé est crée, il doit impérativement être libéré par le code.
    Le Garbage Collector ne libère que les TLS anonymes.

    Ressources:
    SignalAndWait
    SignalAndWait est une instruction qui permet à deux threads de s'offrir "un point de rendez-vous". Cette instruction oblige deux threads à s'attendre l'un l'autre.
    Chacun des thread détient un AutoResetEvent (il y en a deux, un par thread).
    Avec SignalAndWait, chacun des threads signal l'AutoResetEvent de l'autre thread et attend que l'autre thread signal son propre AutoResetEvent.

    Ressources:
    • Sync C# - SignalAndWait exemple (a faire)
      Patterns de synchronisationA mis chemin entre la synchronisation et l'implémentation de threadind, voici quelques patterns bien utiles.

      Acknowledgement Pattern (Ready/Go)
      Ce pattern permet à deux processus (threads ou logiciels) de mettre de se transmettre une ressource au moment approprié (par exemple le contenu du fichier c:\temp\data.txt).
      La métaphore du contrôle technique :
      Pour employer une métaphore appropriée, nous allons considérer le passage de véhicules au contrôle technique tel qu'il est organisé en Belgique.
      L'employé du contrôle technique sera le premier processus (process CT) tandis que le client dans sa voiture sera le second (process Customer).
      C'est bien connu, au contrôle technique, il y a toujours la file et le premier de la file attends patiemment qu'on l'invite à avancer.
      C'est le signal "Ready" lancé par l'agent du contrôle technique. Jusqu'à ce signal, il n'est pas autorisé de s'avancer (placer une ressource) sur la ligne de contrôle.
      Lorsque la voiture est placée sur la ligne du contrôle technique (mise a disposition de la ressource) on arrête le moteur et sans le savoir le conducteur donne le signal du départ "GO" pour débuter le contrôle technique.
      A ce moment, et jusqu'à la fin du contrôle technique, la voiture (ressource) passe sous le contrôle exclusif de l'agent du contrôle technique.
      Avantages: facile a mettre en œuvre, limite le nombre de threads, permet d'alerter un background worker d'une tâche a effectuer, sérialise l'exécution des taches.
      Inconvénient: Synchronisation bloquante et donc perte de réactivité (voir alternative "wait and pulse"), sérialisation des appels... pas de queue de traitement (voir alternative "Producer/Consumer QUEUE").
       
      Ressource:
      ProducerConsumerQueue
      L'un des terribles désavantages du pattern Acknowledgment, c'est que le thread générant les tâches (producer) doit impérativement attendre que le thread de traitement (consumer) ait terminé le traitement.
      Si cela peut répondre a un certains nombre de cas pratiques, cela n'est pas des plus optimal.
      Le pattern ProducerConsumerQueue met en place une queue de traitement. Le thread producer peut ajouter tranquillement les tâches dans la queue de traitement sans se soucier du thread qui les traitent (Consumer).
      A noter que le framework .Net contient une classe ProducerConsumerQueue.
      Ressource:
      • Sync C# - ProducerConsumerQueue
        Description plus précise et exemple didactique montrant la mécanique interne d'un ProducerConsumerQueue.
        Mise en place d'un ProducerConsumerQueue avec 1 thread producer et 1 thread consumer.
        Mise en place d'un ProducerConsumerQueue avec plusieurs threads producer et 1 thread consumer.
      • Sync C# - Multi-Producer Multi-Consumer Queue
        Version améliorée du ProducerConsumerQueue supportant plusieurs Producer Threads et plusieurs Consumer Threads (worker threads).

      Wait And Pulse
      C'est une méthode de synchronisation dite non-bloquante.
      En effet, dans les scénarios classiques de threading, une synchronisation est dite "bloquante" (par exemple pour accéder à une variable partagée ou l'utilisation d'EventHandle).
      Le thread bloqué (en attente de la synchro) ne consomme plus de ressource CPU mais est déschédulé de la pile d'exécution dans l'attente de la libération de la ressource.
      Dans un environnement à fort accès concurrent, cela peut représenter un désavantage car le thread bloqué est retiré pendant un certain laps temps par le scheduler (alors que la synchronisation pourrait être obtenue dans un délai très court).
      Pour répondre à ce problème spécifique, le framework .Net à mis en place la synchronisation Wait and Pulse permettant à deux (ou plusieurs threads) de partager en même temps le même objet de synchronisation (lock) et de s'avertir mutuellement de la libération de la ressource (en évitant aux threads d'être dé-schedulé).

      L'implémentation d'un Wait And Pulse doit scrupuleusement suivre le design pattern.
      En effet, comme les deux threads partagent l'intérieur de la même section critique (lock), ne pas suivre scrupuleusement le pattern causerait inévitablement des problèmes de synchronisations (et beaucoup de cheveux blancs).

      Ressources:

      Encore à traiter
      • Locking, les objets de synchronisation, Join
      • Interlocked et Volatile
      • L'attribut System.ThreadStatic

      Aucun commentaire: