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
}

Aucun commentaire: