lundi 25 octobre 2010

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.

Aucun commentaire: