jeudi 28 octobre 2010

Sync C# - ProducerConsumerQueue

Préambule
L'article "Acknowledgement pattern (Ready/Go)" montrait un mécanisme de synchronisation permettant à deux threads (processus) de se synchroniser pour permettre la communication d'une tâche.
Pour rapperl, le Thread producer devant attendre le signal "ready" avant de présenter sa tâche au thread Consumer (worker). A son tour, le thread Consumer devait attendre le signal "go" pour utiliser la tâche et effectuer son processing.
Cette implémentation à cependant un terrible désavantage, le thread producer est obligé d'attendre la fin du processing par le thread consumer (le worker) avant de pouvoir présenter une nouvelle tâche... c'est terriblement bloquant (1*).
Le pattern ProducerConsumerQueue apporte une réponse à ce problème.

Description
Le pattern ProducerConsumerQueue est une très nette amélioration du pattern Acknowledgment.
Le ProducerConsumerQueue permet au thread producer (celui qui prépare les tâches) d'empiler les tâches dans une queue de traitement sans avoir a se soucier de l'état d'avancement du thread consumer (le worker thread qui traite les tâches).
Une telle structure permet également d'avoir:
  • Plusieurs threads producer (threads qui ajoutent les tâches).
    C'est très utile lorsque la phase de préparation des tâches est plus longue que la phase de traitement.
    Par exemple, l'interprétation d'un fichier (producer) vs Injection des données dans une BD (producer).
  • Plusieurs threads consumer (threads qui traitent les tâches) moyennant quelques adaptations du code.
    Très utile si le traitement ne consomme pas trop de ressources mais est néanmoins long.
    Par exemple, préparation de fichiers de commandes vs communication avec fournisseurssss à l'aide de modems.

Si le Framework .Net dispose effectivement d'une classe ProducerConsumerQueue, cet article abordera malgré tout le fonctionnement détaillé qu'une telle structure. Il est en effet plus simple par la suite d'en comprendre l'utilité.

Exemple - Single Producer, Single Consumer
Cet exemple montre l'implémentation d'un ProducerConsumerQueue dans la classe ProducerConsumerQueue.
  • La classe est Disposable (ce qui permet de libérer le worker thread).
  • La queue dans laquelle s'entassent les tâches est bien évidemment protégée par un lock (ce qui évite des races conditions durant l'empilement/dépilement). 
  • Le worker thread (consumer) utilise un AutoResetEvent pour être averti qu'il y a une tâche à traiter.
  • Le worker thread (consumer) continue le traitement tant qu'il y a une tâche dans la queue.
    Lorsque la queue est vide, le thread attend le signal de l'AutoResetEvent (ce qui indique qu'une nouvelle tâche vient d'être ajoutée).
Le snippet utilise la classe ProducerConsumerQueue pour en démontrer l'usage d'un ProducerConsumerQueue.

Fichier:  Threading_ProducerConsumerQueue.cs
using System;
using System.Collections.Generic;
using System.Threading;
using System.Linq;

public class ProducerConsumerQueue : IDisposable {
    Queue<string> tasks = new Queue<string>(); // Tasks queue, enqueue a NULL to exit worker thread
    EventWaitHandle wh = new AutoResetEvent(false); // Will signal worker thread that new Tasks are present.
    Object locker = new Object();
    Thread worker;
    
    /// <summary>
    /// CTor. Start the worker thread
    /// </summary>
    public ProducerConsumerQueue() {
        worker = new Thread( DoWork );
        worker.Start();
    }
    
    /// <summary>
    /// Enqueue some work
    /// </summary>
    /// <param name="sTaskToPrint"></param>
    public void EnqueueTask( string sTaskToPrint ){
        lock( locker ) {
            tasks.Enqueue( sTaskToPrint );
            wh.Set(); // if worker was blocking (because no more work), signal that new work arrived.
        }    
    }
    
    /// <summary>
    /// Enqueue a NULL to inform the worker thead to Exit
    /// </summary>
    public void EnqueueExitThread() {
        EnqueueTask( null );
    }
    
    /// <summary>
    /// Dispose the Object
    /// </summary>
    public void Dispose() {
        // Signal the consumer to exit
        EnqueueExitThread();
        // Wait worker thread to finish
        worker.Join();
        // Release OS ressource
        wh.Close();
    }
    
    /// <summary>
    /// Method that implement the Consummer
    /// </summary>
    public void DoWork(){
        while(true) {
            string task = null;
            bool waitSignalToContinue = false; // Consumer waits for signal (new item added) to continue to consume
            
            // Dequeue the task
            lock( locker ) {
                if( tasks.Count == 0 ) {
                    // No more work, thread should wait
                    waitSignalToContinue = true;
                }
                else {
                    // Make a copy of the task
                    string temp = tasks.Dequeue();
                    if( temp != null ) task = String.Copy( temp  );
                    
                    // Check exit condition (dequeue a NULL)
                    if( task == null )
                        return;
                }                            
            }
            

            // Wait for signal must be performed OUTSIDE the lock !
            if( waitSignalToContinue ) {
                // if no more task in the list 
                //   --> Block the thread and wait signal for new job to do                
                wh.WaitOne(); 
            }            
            
            // Execute the task
            //    task may be null if the Queue is empty
            if( task != null ){
                Console.WriteLine( "Performing task: "+ task );
                Thread.Sleep( TimeSpan.FromSeconds( 1 ) );
            }        

        }
    }
}

public class MyClass
{
    public static void RunSnippet()
    {
        // Using WL() seems to cause troubles
       WL( "Producer -- first Enqueue" );
        // Creating the Producer-Consumer Queue
        //   And proceed
        using( ProducerConsumerQueue q = new ProducerConsumerQueue() ){
            q.EnqueueTask( "Beginning" );
            foreach( int i in Enumerable.Range( 1, 6 ) )
                q.EnqueueTask( String.Format( "First Enqueue for {0}", i ) );
            q.EnqueueTask( "Pausing producer" );
            Thread.Sleep( TimeSpan.FromSeconds( 2 ));
            // WL( "Producer -- second Enqueue" );
            foreach( int i in Enumerable.Range( 25, 4 ) )
                q.EnqueueTask( String.Format( "Second Enqueue for {0}", i ) );
            q.EnqueueTask( "That's all folks" );
            WL( "Producer -- Enqueue finished" );
        } // The usage of USING will enforce q.Dispose (that join the worker thread)
        WL( "Producer -- ProducerConsuumerQueue disposed" );
    }
    
    #region Helper methods
    ...
    #endregion
}

Ce qui produit le résultat suivant:
Producer -- first Enqueue
Performing task: Beginning
Performing task: First Enqueue for 1
Producer -- Enqueue finished
Performing task: First Enqueue for 2
Performing task: First Enqueue for 3
Performing task: First Enqueue for 4
Performing task: First Enqueue for 5
Performing task: First Enqueue for 6
Performing task: Pausing producer
Performing task: Second Enqueue for 25
Performing task: Second Enqueue for 26
Performing task: Second Enqueue for 27
Performing task: Second Enqueue for 28
Performing task: That's all folks
Producer -- ProducerConsuumerQueue disposed
Press any key to continue...


Le vilain petit DeadLock
En travaillant sur une version MultiProducer de l'algorithme, je me suis aperçu qu'il était impossible de redémarrer une série de traitement lorsque le worker thread avait terminé sa première volée de tâche.
De même, quelques instructions d'affichage avant d'ajouter la première tâches dans la queue produisait le même effet (freeze).
Il y avait donc un bug dans cette première version.
Tout cela était dût à un DeadLock dans le worker dont le petit bout de code fautif est repris ci-dessous:
 public void DoWork(){
        while(true) {
            string task = null;
            // Dequeue the task
            lock( locker ) {
                if( tasks.Count == 0 ) {
                    // if no more task in the list 
                    //   --> Block the thread and wait signal for new job to do                
                    wh.WaitOne(); 
                }
                else {
                    // Make a copy of the task
                    string temp = tasks.Dequeue();
                    if( temp != null ) task = String.Copy( temp  );
En quelques mots:
  • S'il n'y a plus rien a traiter dans la queue, le worker thread passe en état bloqué (instruction WaitOne).
  • Mais cet état bloqué intervient alors qu'un lock est maintenu sur l'objet "locker".
    Le thread est dé-schédulé avec un lock actif :-/ (alors qu'on est pas dans un pattern WaitAndPulse où cela est possible).
  • Le DeadLock intervient lorsque l'on exécute la méthode ProducerConsumerQueue.EnqueueTask.
    En effet, pour pouvoir empiler la tâche en toute sécurité, la méthode à aussi besoin d'acquerir le lock sur l'objet "locker" (qui restera locké Advitam Eternam par le worker thread!).
Exemple - Multiple Producer, Single Consumer

Le fichier Threading_MultiProducerConsumerQueue.cs contient un exemple ou la queue de la classe ProducerConsumerQueue est alimentée par plusieurs threads.

Fichier: Threading_MultiProducerConsumerQueue.cs

Seule la section de code concernant le corps du snippet et les threads d'empilement des tâches sont repris a titre indicatif.

/// <summary>
/// Parametrization Data for EnqueueThread
/// </summary>
public class EnqueueThreadStartData {
    public  EnqueueThreadStartData ( ProducerConsumerQueue q, int startIndex ) : base() {
        Queue = q;
        StartIndex = startIndex;
    }
    
    public ProducerConsumerQueue Queue { get; set; }
    public int StartIndex { get; set; }
}

/// <summary>
/// Test class containing the snippet
/// </summary>
public class MyClass
{
    public static void RunSnippet()
    {
        ProducerConsumerQueue q = new ProducerConsumerQueue();
        
        // Enqueue data with threads
        Thread thEnqueue1 = new Thread( new ParameterizedThreadStart( DoWork_Enqueue ) );
        Thread thEnqueue2 = new Thread( new ParameterizedThreadStart( DoWork_Enqueue ) );
        
        thEnqueue1.Start( new EnqueueThreadStartData( q, 1000 ) );
        thEnqueue2.Start( new EnqueueThreadStartData( q, 2000 ) );
        thEnqueue1.Join();
        thEnqueue2.Join();
        WL( "Enqueue Finished");
        
        WL( "Press Enter to enqueue a new values" );
        RL();
        Thread thEnqueue3 = new Thread( new ParameterizedThreadStart( DoWork_Enqueue ) );
        thEnqueue3.Start( new EnqueueThreadStartData( q, 555 ) );
        
        // WARNING:
        //  if we omit to join, the q.Dispose will dispose the worker Thread (the consumer)
        //  and ressources while the thEnqueue3 (producer) is still adding data to the queue.
        //  THIS WOULD RESULT IN APPLICATION CRASH!
        thEnqueue3.Join();
        
        // Join worker thread and release resource
        q.Dispose(); 
    }
    
    private static void DoWork_Enqueue( object threadStartData ) {
        // cast data object
        EnqueueThreadStartData data = (EnqueueThreadStartData)threadStartData;
        
        WL( string.Format( "Enqueue Thread {0} started. Will enqueue values from index {1}", Thread.CurrentThread.GetHashCode(), data.StartIndex ) );
        
        // Enqueue values 
        foreach( int i in Enumerable.Range(  data.StartIndex, 25 )) {
            // Enqueueing is reentrant [thread safe] because of internal lock on the queue
            WL( string.Format( "Thread {0} Enqueue value {1}", Thread.CurrentThread.GetHashCode(), i ) );
              data.Queue.EnqueueTask( string.Format("task #{0}", i ) );
            // Stimulate context switching
            Thread.Sleep( TimeSpan.FromMilliseconds(50) );
        }
        WL( string.Format( "Enqueue Thread {0} finished", Thread.CurrentThread.GetHashCode() ) );
    }
    
    #region Helper methods 
    ...#endregion
}

Notes de bas de page
1*: Quel jeu de mot :-) ! 
C'est en effet bloquant d'un point de vue logiciel puisqu'il y a des moments d'attente manifeste dans le soft. Mais c'est également bloquant parce que la synchronisation est faite avec des AutoResetEvent qui sont des procédés de synchro dit "bloquant", le thread est dé-schédulé jusqu'au signalement de l'évènement.

mercredi 27 octobre 2010

Sync C# - Acknowledgement pattern (Ready/Go)

Description
Un peu a cheval entre les méthodes de threading et les méthodes de synchronisation, voici le pattern "acknowledgement" qui utilise deux AutoResetEvent (signaux) nommés "Ready" et "Go".
C'est justement à cause de ces signaux que je préfère nommer ce pattern "Ready/Go" car c'est nettement plus parlant une fois que l'on a compris son fonctionnement.
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).
Il permet de mettre en place une queue de traitement en série de type Producer-Consummer (dans sa version la plus simple bien entendu).

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'a la fin du contrôle technique, la voiture (ressource) passe sous le contrôle exclusif de l'agent du contrôle technique.

En résumé :
Le signal Ready est envoyé par le consummer (process CT) pour indiqué que la ressource a traiter peut être mise à disposition.
Le producer (process Customer) doit attendre ce signal avant de placer sa ressource à l'emplacement désigné.
Le signal Go est envoyé par le producer (process Customer), celui-ci informe le consumer (process CT) que la ressource est en place et librement accessible.
Le consumer (process CT) n'est pas autoriser à prendre possession de la ressource et à la manipuler tant que le producer (process Customer) n'a pas donné le signal du départ.

mais encore :
Le processus Producer et Consumer sont deux threads distincts (un thread producer et un thread consummer).
Le thread Producer est bloqué tant que le thread consumer n'a pas terminé son traitement sur la ressource (cela peut être un avantage dans certaines situations).
Ces deux threads partagent une ressource commune (ex: une string déclarée comme volatile).
Du fait du type même de cette synchronisation, la ressource partagée ne doit pas être protégée à l'aide d'un Lock.
Finalement, il est assez usuel de demander l'arrêt du thread Consumer en lui passant une référence nulle (en lieu et place de la ressource a traiter)

Avantages et inconvénients
Avantage:
Avec ce procédé, il n'est pas nécessaire de créer un thread différent pour chaque ressource à traiter. Il y a un thread producer et un thread consumer. Cela minimise l'impact de création des threads.
Avantage:
Permet de serialiser le traitement et permet également de limiter la consommation des ressources.
C'est pratique pour des traitement imposant un traitement de type "série" (ex: spooler d'imprimante), traitement de nombreux fichiers en limitant les IO (un thread collecte et prépare les fichiers, un second les traitent/transforment).
Avantage:
Evite d'éventuelles interactions indésirables entre de multiples workers (ce qui serait potentiellement le cas si un thread était créé par ressource à traiter).
Avantage:
Comme le pattern utilise des WaitHandle, il n'y a pas de consommation cpu inutile pendant les opérations d'attentes (cfr méthode WaitOne).
Comme ces synchronisations sont blocantes, le scheduler peut effectuer du context switching et utiliser le temps machine pour un autre thread.
Inconvénient:
Ce pattern n'autorise qu'un seul producer et un seul consumer.
Inconvénient:
Il n'est pas possible d'empiler les traitements à faire.
Tant que le producer ne reçoit pas le signal "Ready" du consummer, il lui est interdit de présenter un nouvelle ressource à traiter.
Il existe cependant un pattern alternatif nommé "Producer/Consummer QUEUE" qui permet d'empiler les ressources à traiter... ces dernières seront pris en charge par un background worker thread.
Inconvénient:
Puisqu'il a justement du context switching (voir avantages ci-avant), le thread bloqué doit éventuellement être rescheduler pour exécution. Ce qui peut représenter une perte de temps non négligeable si le logiciel doit avoir une forte réactivité.
Pour ces cas plus extrême, il existe le pattern "Wait And Pulse" qui est une des synchronisation non bloquante. 

Exemple
L'exemple suivant présente une implémentation rudimentaire du pattern Acknowledgement (Ready/Go).
Fichier: Threading_ReadyGo_triggering.cs

using System;
using System.Collections.Generic;
using System.Threading;

/// <summary>
/// This simple projet show a rudimentary synchronization between a Worker Thread and the Main Thread.
/// The threads are waiting each other.
/// The main thread waits the Worker Thread to be ready to receive the next task to accomplish.
///     During the worker thread processing, the main thread does not change anything!
/// The worker thread on start the execution of the new task (the Go)
///     This allow the main thread to safely initialize the shared properties
/// 
/// This triggering process is based on two AutoResetEvents (named "Ready" & "Go")
/// </summary>
public class MyClass
{
    // Worker Thread Informing main thread that he is ready for new work (share variable can be assigned)
    static EventWaitHandle ready = new EventWaitHandle( false, EventResetMode.AutoReset);
    // Main thread alerting Worker Thread that new work can start (shared variable assigned)
    static EventWaitHandle go = new EventWaitHandle( false, EventResetMode.AutoReset);
    // Share variable (containing the work to do by the working thread).
    //    Use NULL to signal END of work
    static volatile string TaskInfo = "";
    
    public static void RunSnippet()
    {
        // Create the worker (consumer) thread
        Thread th = new Thread( Worker );
        th.Start();
        for(int i = 0; i < 10; i++ ){
            // Wait the worker to be ready for a new task
            ready.WaitOne();
            // Set the task information
            TaskInfo = "A".PadRight(i+1, 'h');
            // Signal Worker Thread to GO
            go.Set();
        }
        
        // Finally, signal worker thread to END
        ready.WaitOne(); TaskInfo = null; go.Set();
        // Wait worker thread to end
        th.Join();

    }
    
    /// <summary>
    /// Worker thread (the consumer) handling the tasks
    /// </summary>
    static void Worker(){
        while( true ){
            // Inform main thread that worker is ready for new task
            ready.Set();
            // Wait Main Thread to initialize variables (and inform worker to Go).
            go.WaitOne();
            // Check for stop instruction
            if( TaskInfo == null )
                return;
            // Perform the task
            WL( TaskInfo );
        }
    }
    
    
    #region Helper methods
    ...
    #endregion
}

mardi 26 octobre 2010

Sync C# - Semaphore

Description du 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.
Lorsque le sémaphore est créé, il faut indiquer la capacité totale et le nombre de place disponible.
Dans la pratique, un sémaphore décompte le nombre de places disponibles au fur qu'il les allouent (qu'il laisse rentrer les threads).
Ainsi, pour un sémaphore de capacité 3. Le nombre de place disponible est 3 si aucune des places n'a été demandée. Si deux threads demandent d'entrer dans le sémaphore (la boite de nuit) avec l'instruction WaitOne, ils rentrerons et le nombre de place disponible tombe à 1.
Si deux nouveaux threads demandent alors à  rentrer, le premier obtient son droit de passage et le sémaphore tombe à 0 place disponible. Le deuxième thread doit attendre à la porte qu'un des trois premier threads sorte du sémaphore (ce qui aura pour effet d'incrémenter le nbre de place disponible dans le sémaphore et d'autoriser une nouvelle entrée).

Cas d'utilisation de Sémaphore
Limitation de charge
ne pas autoriser plus d'un certains nombre d'exécutions en parallèle pour limiter la charge sur le système.
Par exemple traiter une série de fichiers (ex: 35) à l'aide de threads ThreadPool.QueueUserWorkItem (un par fichier) mais limiter le traitement concurrent à deux fichiers en même temps avec un sémaphore à deux places (car les I/O, ca coute cher en ressource).

Utiliser comme Mutex
Probablement inutile car la plateforme .Net dispose d'une classe Mutex.
Déclarer un Sémaphore avec une capacité de 1 fonctionnera comme un mutex.

Synchronisation des threads
Il est possible de synchroniser des tâches (threads) à l'aide d'un Sémaphore créé comme n'ayant plus de place disponible (mais une capacité de 1).
Ainsi, si le Thread 1 doit impérativement terminer sa tâche avant le démarrage du thread 2:
  1. Un sémaphore de capacité 1 et place disponible est créer.
  2. Thread 1 est démarré
  3. Thread 2 est démarré, sa première instruction est de vouloir entrer dans le sémaphore. 
Lorsque le thread 1 aura terminé sa tâche, il quitte le sémaphore.
Cela libère une place dans le sémaphore et permet au thread 2 d'y rentrer.
Thread 2 à donc bien attendu la fin de l'exécution de la première tâche.
 Ce procédé est appelé "Barrier" (barrière) et permet visiblement la synchronisation de plusieurs tâche.
Note:
Un résultat identique peut-être obtenu à l'aide de l'utilisation Manual/Auto ResetEvent avec deux cas de figures:
1) Soit avec un AutoResetEvent + ThreadPool.RegisterWaitForSingleObject.
2) Soit un AutoResetEvent + le thread 2 deja démarré qui fait un WaitOne sur l'AutoResetEvent


Exemple
Dans cet exemple, on utilise le threadpool à l'aide de Async Delegate Invocation pour démarrer 25 traitements différents.
Comme on utilise le ThreadPoll, plusieurs traitement seront exécutés en parallèle à la discrétion du ThreadPool lui même.
L'exemple utilise un sémaphore pour limiter le traitement en concurrence à deux instances de traitement.
Qu'il soit bien clair que cet exemple limite le traitement en parallèle et non le nombre de threads.
Par ailleurs, comme les threads en attentent sur le sémaphore utilisent l'instruction WaitOne (instruction dite bloquante/blocking), ces threads sont sujets au "context switch" et seront dé-schédulés... il ne consommeront donc pas inutilement du temps processeur.

Résultat sans usage du sémaphore
Sans usage du sémaphore, l'on constate que plusieurs threads entament leur traitement en même temps.
La première salve de traitement concurrent est marqué comme ceci.

Thread 4 is processing ressource MAN: Metropolitan Area Network
Thread 3 is processing ressource MACAO : Méthode d'analyse et de con
plications orientées objet
Thread 5 is processing ressource MAO : Musique assistée par ordinate
Thread 6 is processing ressource MAPI : Microsoft Messaging Applicat
ing Interface
Thread 7 is processing ressource Mb : mégabit
Thread 8 is processing ressource Mbit : mégabit
Thread 9 is processing ressource Mbit/s : mégabit par seconde
Thread 10 is processing ressource MBR : Master Boot Record
Thread 11 is processing ressource MBSA : Microsoft Baseline Security
Thread 4 finished
Thread 4 is processing ressource MCD : Modèle Conceptuel de Données
e MERISE)
Thread 3 finished

Résultat avec sémaphore
Par contre, avec le sémaphore activé, seul deux threads effectuent leur traitement en même temps.
La première salve de traitement concurrent est indiqué comme cecila seconde comme cela et la troisième comme ceci.
L'on constate aisément qu'il n'y a que deux traitements exécutés en parallèle.

Thread 4 is processing ressource MAN: Metropolitan Area Network
Thread 3 is processing ressource MACAO : Méthode d'analyse et de co
plications orientées objet
Thread 4 finished
Thread 4 is processing ressource MCD : Modèle Conceptuel de Données
e MERISE)
Thread 3 finished
Thread 6 is processing ressource MAPI : Microsoft Messaging Applica
ing Interface
Thread 3 did extract MACAO from MACAO : Méthode d'analyse et de con
lications orientées objet
Thread 4 did extract MAN from MAN: Metropolitan Area Network
Thread 4 finished
Thread 4 is processing ressource MD4 : Message Digest 4
Thread 6 finished
Thread 6 is processing ressource MD5 : Message Digest Version 5
Thread 4 finished
Thread 19 is processing ressource MDI : Multiple Document Interface
Thread 6 finished

Code Source
Fichier:  Threading_Semaphore.cs
using System;
using System.Collections.Generic;
using System.Threading;

/*
  Sample demonstrating usage of Semaphore.
  This sample will start 25 request on the threadpool to  a worker (to work on 25 different ressources) 
  but the coder did decided to run only 2 concurrent processing at the same time (to limits ressource consumption)
  whatever the number of threads is allocated.
  
  This limitation is taken in charge with a Semaphore.

  Note: This sample also use the Asynchronous Delegate Pattern to befenits from ThreadPoll
        http://domeu.blogspot.com/2010/09/threading-en-c-exemple-asynchronous.html
*/

/// <summary>
/// Declare the Worker delegate for Asynchronous Delegate invocation
/// </summary>
public delegate string Worker ( string name );

public class MyClass
{
    // switch it to false to see how the processing is handled without the Semaphore
    private const bool USE_SEMAPHORE = true; 
    // Semaphore Available=2, Capacity=2
    private static Semaphore ConcurrentProcessingSemaphore = new Semaphore( 2, 2 ); 
    // Sample data
    private static List<string> nameList = new List<string>() { 
        "MACAO : Méthode d'analyse et de conception d'applications orientées objet", "MAN: Metropolitan Area Network", "MAO : Musique assistée par ordinateur", "MAPI : Microsoft Messaging Application Programming Interface", "Mb : mégabit",
        "Mbit : mégabit", "Mbit/s : mégabit par seconde", "MBR : Master Boot Record", "MBSA : Microsoft Baseline Security Analyzer", "MCD : Modèle Conceptuel de Données (voir méthode MERISE)",     
        
        "MCGA : Multicolor Graphics Array", "MCH : Memory Controller Hub", "MCI : Multimedia Command Interface", "MCSE : Microsoft Certified Systems Engineer", "MCT : Microsoft Certified Trainer",
        "MD4 : Message Digest 4", "MD5 : Message Digest Version 5", "MDA : Model driven architecture", "MDF : Meta Data Framework (Cisco)", "mdk : Mandrakelinux",
        
        "MDI : Multiple Document Interface", "MDR : Mort De Rire", "MFLOPS : Million Floating Point Operations Per Second", "MFM : Modified Frequency Modulation",     "MH : Mail Handler"
    };
    
    public static void RunSnippet()
    {
        List<IAsyncResult> asyncResults= new List<IAsyncResult>();
        List<Worker> workers = new List<Worker>();
        
        //=== Start the Threads ====================
        // use threadpool for executing (with Asynchronous delegate).
        foreach( string sValue in nameList ) {
            // Instanciate the Delegate
            //    and point it to the target worker method
            Worker worker = new Worker( DoWork );
            IAsyncResult asyncResult = worker.BeginInvoke( sValue, null, null );
            asyncResults.Add( asyncResult );
            workers.Add( worker );
        }
        
        //=== Doing some processing ================
        // ....
        
        //=== Wait for the results =================
        for( int i = 0; i<asyncResults.Count; i++ ){
            // Retreive Asynch references
            Worker worker = workers[i];
            IAsyncResult asyncResult = asyncResults[i];
            string result = string.Empty;
            // EndInvoke
            result = worker.EndInvoke( asyncResult );
            // Display result
            WL( result );
        }
    }
    
    public static string DoWork( string name ){
        if( USE_SEMAPHORE )
            ConcurrentProcessingSemaphore.WaitOne();
        try {
            WL( string.Format("Thread {0} is processing ressource {1}", Thread.CurrentThread.GetHashCode(), name) ); 
            // Using long delay will cause the ThreadPool 
            // to allocate others threads for the other asynch invocation
            Thread.Sleep( TimeSpan.FromSeconds(4) );
            WL( string.Format("Thread {0} finished", Thread.CurrentThread.GetHashCode()) ); 
            return string.Format( "Thread {0} did extract {1} from {2}", Thread.CurrentThread.GetHashCode(), name.Split(':')[0].Trim(), name ); // Simulate modification of value
        }
        finally {
            if( USE_SEMAPHORE )
                ConcurrentProcessingSemaphore.Release();
        }
    }
    
    #region Helper methods
    ...
    #endregion
}

lundi 25 octobre 2010

Sync C# - Mutex

Description du Mutex
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.
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.
Il permet de créer des sections critiques comme le fait l'instruction lock mais avec quelques différences notables; à savoir qu'un mutex est beaucoup plus lent et couteux en ressource qu'un lock.
Cette lenteur du mutex est due au fait que les mutexes sont gérés par le système d'exploitation alors que le lock est propre à la plateforme .Net.
Ce désavantage de la lenteur inhérent au système d'exploitation est aussi une grande force puisque deux processus distinct fonctionnant sur une même machine (et même écrits dans des langages différents) peuvent synchroniser leurs accès à une ressource commune à l'aide de Mutex.

Utilisation des Mutexes en .Net
Au sein d'un même logiciel .Net, partager la référence vers l'instance de l'objet Mutex est suffisant pour faire la synchronisation.
Par contre, si le Mutex doit être utilisé par d'autres logiciels ou d'autres instances d'un même logiciel, il est alors nécessaire de le nommé. C'est le nom du mutex qui sera utilisé par le Kernel Windows pour faire les synchronisation.

Exemples
Exemple de Named Mutex
Cet exemple démontre l'usage des named mutex.
Le but ici est de détecter de façon assez rudimentaire si l'application fonctionne déjà. Essayez donc de démarrer en même temps deux instances de ce snippet
Note: il serait également possible de le savoir en consultant le compteur d'instance.

Fichier: Threading_NamedMutex.cs


using System;
using System.Collections.Generic;
using System.Threading;

public class MyClass
{
    static Mutex mutex = new Mutex( false, "Blog.Domeu.Net-OneAppAtTheTime" );
    
    public static void RunSnippet()
    {
        WL( "Starting" );
        // Detect if sofware is already running
        //   Tested during some seconds (in case when other software is shuting down).
        if( !mutex.WaitOne(TimeSpan.FromSeconds(5) , false )){
            WL("Oups, another software is already running");
            return;
        }
        WL( "Started" );
        WL( "Press enter to exit");
        RL();
        // Always release the mutex
        //    Otherwise other processed attempting to acquire it will received an exception 
        //    when the mutex would be abandoned by this process.
        mutex.ReleaseMutex();
        
            
        
    }
    
    #region Helper methods 
    ...
    #endregion
}

Exemple de partage de référence
Cet exemple démontre l'usage d'un mutex non nommé.
Ici, le partage de la référence du mutex permet a un seul des deux threads (a la fois) de modifier une variable globale.
Note: cet exemple est a pure usage didactique, l'utilisation d'un lock aurait été beaucoup plus efficient.

Fichier: Threading_UnNamedMutex.cs

/*  This sample demonstrate the usage of UN-NAMED MUTEX having a reference shared among the threads.

    The sample is simple, each thread can increase the value of a shared variable (SharedValue).
    A mutex is used to avoids race condition.
    
    NB: Usage of Lock instruction would give the same result with better performance but mutex was use 
        for the purpose of the sample.
*/
using System;
using System.Collections.Generic;
using System.Threading;

public class MyClass
{
    private static Mutex sharedMutex = new Mutex();
    private static int SharedValue = 0; 
    
    public static void RunSnippet()
    {
        List<Thread> threadList = new List<Thread>();
        // Create Threads
        for( int i = 1; i<=2; i++ ){
            Thread aThread = new Thread( new ThreadStart( DoWork ) );
            threadList.Add( aThread );
        }
        
        // Start the threads
        foreach( Thread th in threadList )
            th.Start();
        
        // Wait that all threads get finished
        threadList.ForEach( th => th.Join() );
        
    }
    
    public static void DoWork() {
        int value = 0;
        while( value < 1000 ) {
            // Update the SharedValue + take a copy
            sharedMutex.WaitOne(); // Use the mutex
            SharedValue++;
            value=SharedValue;
            sharedMutex.ReleaseMutex(); 
            // Display value every 10 occurences
            if( (value%10) == 0 )
              WL( String.Format( "Thread {0} updated SharedValue to {1}", Thread.CurrentThread.GetHashCode(), value ));
        }
        WL( String.Format("Thread {0} get completed", Thread.CurrentThread.GetHashCode() ) );
    }
    
    #region Helper methods 
    ...
    #endregion
}

Sync C# - ReadWriteLockSlim

Un cas de figure

Imaginons qu'il faille faire de temps en temps une mise à jour d'une structure de donnée en mémoire, structure qui maintient une série de paramètres pour une application multi-threads.
Sans aucun mécanisme de protection, les paramètres pourraient êtres lus par l'un ou l'autre thread pendant qu'ils sont rechargés. Une telle concurrence appelé "race condition" débouchera inévitablement sur une exception qui plantera le thread (et l'application par la même occasion si les exceptions ne sont pas gérées de façon appropriées dans le thread).

Utiliser l'instruction lock
La méthode la plus simple est encore d'utiliser l'instruction lock pour restreindre stratégiquement l'accès aux informations.  Semblable au mutex sur le principe (mais tellement plus rapide), le lock permet de définir une sections critique accessible par un seul thread à la fois.
Ainsi, durant le rechargement, on peut être certains qu'il n'y aura pas de lecture depuis un autre thread... et donc pas de risque de plantage :-)
Cependant cette méthode de locking a un terrible désavantage.
Elle bloque l'accès en lecture concurrente. Si plusieurs threads doivent accéder aux paramètres, ils se retrouvent tous à attendre à la queue-leu-leu que le lock soit libéré par un thread précédent.
Terriblement inefficace.

Utiliser un ReadWriteLock
Pour ce type de cas où il y a plus d'accès en lecture que d'accès en écriture, il existe un type de synchronisation particulier qui est le "Multi-Reader-Exclusive-Writer synchronisation".
Dans le framework .Net, cette synchronisation est prise en charge par les classes ReadWriteLockSlim et ReadWriteLock.

Description du 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.  

Comme beaucoup de procédé de synchronisation, il est possible d'attendre indéfiniment l'accès ou de faire un essais en attendant un certain nombre de millisecondes.

Pour information ReadWriteLockSlim est une version améliorée du ReadWriteLock. Comme le laisse présager le suffixe SLIM, sont fonctionnement est moins lourd en terme de ressource et donc d'autant plus rapide.
Et en plus d'être plus véloce, ReadWriteLockSlim corrige également un problème historique du ReadWriteLock.

Un exemple de Race Condition
L'exemple Threading_ReadWriteLockSlim_WithRace.cs met en place un accès multi-thread concurrentiel à une information partagée dans une classe de configuration (un peu comme cela se présenterait dans un soft d'assez grosse taille).
Cet exemple est codé sans synchronisation de tel façon que si l'on modifie l'information, l'on se retrouve immédiatement dans une "race condition" qui plante l'un des threads de lecture durant la modification (rechargement du paramètre).

L'exemple qui plante:
Source: Threading_ReadWriteLockSlim_WithRace.cs

Soit 10 threads concurrents qui accèdent à un paramètre de l'application.
Ce paramètre est accessible via une propriété qui publie une référence d'un sous objet.
Ce chainage d'objet assez courant dans un gros logiciel réclame néanmoins dans cet exemple un coding un peu alambiqué pour que le snippet reproduise une situation similaire offrant une certitude de race condition.
Donc chacun des 10 threads accèdent ce paramètre de configuration et affiche la valeur du paramètre toutes les 5000 itérations de lectures.
Le thread principal attends un ordre de modification (un caractère au clavier + enter) et modifie le dit paramètre.
Comme il n'y a pas de méthode de synchronisation, il y a inévitablement un ou plusieurs threads de lecture qui explose(nt).



Utiliser le ReadWriteLockSlim
Source: Threading_ReadWriteLockSlim_WithoutRace.cs

Sur base de l'exemple précédent, le code est légèrement modifié pour utiliser un ReadWriteLockSlim dans la classe paramètre.
Ainsi, la propriété offrant l'accès à l'information utilise le ReadWriteLockSlim pour permettre un accès concurrent en lecture et un accès exclusif en écriture.

/// <summary>
/// Shared DataStore class using the singleton pattern
/// 
/// The shared ressouce "SharedText" use a ReadWriteLockSlim to allow concurrent read and signe writer 
/// </summary>
public class DataStore 
{
    private static DataStore instance;
    private static ReaderWriterLockSlim rw = new ReaderWriterLockSlim();
    private static object Locker = new object();
    
    private DataStore(){
        // Use reference to object to mimic real race condition 
        char[] charArray = (".").ToCharArray();
        _SharedRessource = new string( charArray );
    }
    
    /// <summary>
    /// Return the reference of the singleton.
    /// Store your own reference to the instance (because of the locking).
    /// </summary>
    /// <returns></returns>
    public static DataStore Instance 
    {
        get {
            // Many threads accessing this property may cause a race condition
            //    because multiple thread can enter the if condition at same time.            
            lock( Locker ) {
                if( instance == null ) 
                {
                    instance = new DataStore();
                }
                return instance;
            }
        }    
    }  
 // Use reference to object to mimic real race condition
    object _SharedRessource = null; 
    public string SharedText 
    {
        get {
            try {
                rw.EnterReadLock();
                return (string)_SharedRessource; 
            }
            finally {
                rw.ExitReadLock();                
            }                
        }
        set {            
            try {
                rw.EnterWriteLock();
                // --- Set new value by using reference to mimic real race condition ---
                // if we do not use a synch, we will have race conditions for sure
                _SharedRessource = null;
                // mimic processing
                Thread.Sleep( 100 );
                // Set new value 
                char[] charArray = value.ToCharArray();
                _SharedRessource = new string( charArray );
            }
            finally {
                rw.ExitWriteLock();
            }
        }
    }
}


Voir fichier Threading_ReadWriteLockSlim_WithoutRace.cs pour l'exemple complet.

lundi 18 octobre 2010

Threading en C# - exemple event-based asynchronous pattern (AVEC réentrance)

Comme précisé dans l'article "Threading en C# - synchronisation et méthodes de threading", voici un exemple d'implémentation event-based asynchronous pattern (utilisant le ThreadPool) et supportant la réantance.
L'article précédent "Threading en C# - exemple event-based asynchronous pattern (sans réentrance)" implémentait le même pattern mais sans réentrance

Note: les exemples sont développés avec Snippet Compiler.

L'implémentation du pattern  "Event-based asynchronous pattern" n'est pas des plus simple et doit être scrupuleusement suivit.
Cet exemple est issue d'un article MSDN supportant la réentrance et implémentant le pattern pour un composant (ce qui le cas d'utilisation le plus approprié de ce pattern).
Tout comme l'exemple précédent, l'implémentation est faite autour d'une simple classe (au lieu d'un composant).

Code de test
public class MyClass
{
    public static void RunSnippet()
    {
        List<string> sourceList = new List<string>();
        sourceList.Add( "Test 1" );
        sourceList.Add( "This Stuff text" );
        sourceList.Add( "Some other tests" );
        sourceList.Add( "dodo" );


        // Test of the Re-entrant EventBased Asynchronous pattern
        // The unique taskId identifier will be the string reference himself
        TestClass myTest = new TestClass();
        myTest.ComputeTextCompleted += ComputeTextCompletedCallback;
        foreach( String str in sourceList ) {                        
            myTest.ComputeTextAsync( str, str );  // appels multiples :-)
WL( String.Format( "ComputeTextAsync called for \"{0}\"", str ));
        }

        // Cancel while working
        WL( String.Format( "Send cancellation for {0}", sourceList[2] ) );
        myTest.CancelTextAsync( sourceList[2] );
    }
    
    public static void ComputeTextCompletedCallback( object sender, ComputeTextCompletedEventArgs e ) {
        // The unique identifier e.TaskID is the reference to the source text
        if( e.Canceled ) {
            WL( "" );
            WL( String.Format( "ComputeTextCompletedCallback() received cancellation for \"{0}\" :-( ", (string)(e.TaskId) ));
            WL( "" );            
        }
        else {
            WL( "" );
            WL( String.Format( "ComputeTextCompletedCallback() received computed text for \"{0}\"", (string)(e.TaskId) ));
            WL( e.ComputedText );
            WL( "" );
        }
    }
    ...
}

Résultat du code de test
ComputeTextAsync called for "Test 1"
ComputeTextAsync called for "This Stuff text"
ComputeTextAsync called for "Some other tests"
ComputeTextAsync called for "dodo"
Send cancellation for Some other tests
Press any key to continue...  notez que le code appelant s'interrompt ici
ComputeTextCompletedCallback() received cancellation for "Some other tests" :-(

ComputeTextCompletedCallback() received computed text for "This Stuff text"
 *$+-*/This Stuff text*$+-*/

ComputeTextCompletedCallback() received computed text for "Test 1"
 *$+-*/Test 1*$+-*/

ComputeTextCompletedCallback() received computed text for "dodo"
 *$+-*/dodo*$+-*/

Code Source
Fichier: Threading_EventBasedAsynchronousPattern_Reentrancy.cs

Si le code de test démontre le bon fonctionnement de l'ensemble, c'est surtout le code implémentant le pattern qui est le plus intéressant.
Le code n'est pas publié directement dans ce post car il est assez long. Il est par contre accessible via le lien vers le fichier Threading_EventBasedAsynchronousPattern_Reentrancy.cs.

A toute fin utile, le précédent exemple "Threading en C# - exemple event-based asynchronous pattern (sans réentrance)" était accompagné d'un Diagramme de séquence détaillant le fonctionnement du pattern. Le fonctionnement reste identique dans les deux cas (avec et sans réentrance).

Benoît Mandelbrot, le père des fractales, est décédé

Quel informaticien n'a pas un jour croisé le concept des fractales, concept aux nombreuses applications dans le monde réel comme informatique.
Benoît Mandelbrot était le père des théories fractales, domaine mathématique disposant d'une dimension non entière (1 dimension = ligne, 2 dimensions = plan, dimension fractales = 1.1 à 1.9).
Hormis l'économie, la biologie et de nombreux autres domaines, les applications les plus connues de cette théorie reste encore la représentation de la fractale de Mandelbrot (un motif qui se répète à l'infini lorsqu'on la zoom) et l'algorithme de compression jpeg (si je ne dis pas de bêtise).

Le site Futura-science lui  a d'ailleurs dédié un article qui reprend entre autre une interview vidéo.

La vidéo ci-dessous présente le zoom sur un motif fractale (celui de Mandelbrot) et montre sa répétition à l'infini.


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