Le titre de ce post peut paraitre un peu obscur, mais il fait référence à une technique expliquée par Joe Duffy dans son livre Concurrent Programming on Windows (pour info, le monsieur est Lead developper et architecte sur PFX, il envoit sacrément du pâté et son bouquin est un must-read absolu pour tout développeur souhaitant garder son job dans les 5 ans à venir).

 

Un objet privatisable répond astucieusement à la problématique bien connue dans le développement de solutions multi-thread qu’est le partage d’un état en lecture-écriture. Le partage d’état connait deux contraintes importantes :

  • On doit se protéger des Race-conditions (multiples load-store en parallèle)
  • Les modifications d’états composites doivent apparaître de manière atomique (l’objet doit apparaitre dans un état valide et cohérent à tout moment)

Une solution très simple à cette problématique est l’utilisation d’un Mutex ou Monitor (le monitor étant une version allégée du Mutex, nécessitant moins de switch user-mode / kernel-mode):

 

public class Exemple
{
    private MySharedState _sharedState = new MySharedState();

    public void FirstThread()
    {
        // make some long-running task

        // get the exclusive access on _sharedState
        lock (_sharedState)
        {
            // read-write access with mutual exclusion
        }
    }

    public void SecondThread()
    {
        // make some long-running task

        // get the exclusive access on _sharedState
        lock (_sharedState)
        {
            // read-write access with mutual exclusion
        }
    }
}

 

Cette solution bien que fonctionnelle, cette solution pose d’importants problèmes :

  • Si un développeur “oublie” de poser un lock lors d’une lecture-écriture, on peut se retrouver avec un bug de type Race Condition
  • Si un développeur ne pose pas de lock sur un bloc contenant plusieurs lectures de propriétés différentes, il y a un risque d’incohérence (un autre thread aura pu modifier l’état de l’objet entre 2 reads)
  • Un seul thread ne peut entrer dans le lock à un moment T, même si tous les threads concurrents n’ont besoin que d’un accès en lecture (ce qui peut poser un gros problème de performance si la majeur partie des accès se font en lecture seule).

 

Un objet privatisable pallie à tous ces problèmes. L’idée est que l’état partagé est accessible en lecture seule par défaut, et pour pouvoir le modifier, il faut d’abord le “privatiser” (si on essaie de modifier une propriété sans privatiser l’objet auparavant, on reçoit une exception). Privatiser un objet revient à poser un verrou exclusif en lecture-écriture sur l’objet pour le thread courant. Un seul thread peut privatiser l’objet à chaque instant, et pendant toute la durée de la privatisation, les accès à l’objet (en écriture comme en lecture) sont bloqués (grâce à un ReaderWriterLockSlim). Une fois les modifications terminées, le thread “owner” publie l’objet qui redevient accessible en lecture seule pour tout le monde jusqu’à la prochaine privatisation. De plus, plusieurs threads peuvent accéder simultanément en lecture-seule à l’objet, pour éviter des problèmes de performance.

 

Les lectures concurrentes ne s’entre-bloquent pas, les “états intermédiaires” ne sont visibles que par le thread responsable du changement d’état, et les développeurs n’ont pas besoin de poser un “lock” manuellement.

 

Bien sûr, la création d’un tel objet n’est pas forcément super simple, surtout si en plus de ca, l’objet est une source de data-binding dans un client riche (il faut alors retarder les évènements PropertyChanged / CollectionChanged afin que le thread GUI ne soit pas bloqué trop longtemps sur une ré-évaluation de propriété). J’ai donc créé 3 classes réutilisables simplement pour créer des objets privatizables :

  • PrivatizableObject : classe abstraite fournissant le comportement de base, implémentant INotifyPropertyChanged avec retardement, et permettant la privatisation de graphes d’objets complexes
  • PrivatizableList<T> : dérive de PrivatizableObject et implémentant IList<T>
  • PrivatizableObservableCollection<T> : dérive de PrivatizableList<T> et implémente INotifyCollectionChanged avec retardement

Pour créer un PrivatizableObject personnalisé, il suffit alors de dériver de PrivatizableObject, et d’utiliser la syntaxe suivante pour déclarer une propriété :

private class TestablePrivatizableObject : PrivatizableObject
{
    private string _testProperty;
    public string TestProperty
    {
        get
        {
            base.BeginRead();
            string value = _testProperty;
            base.EndRead();
            return value;
        }
        set
        {
            MarkPropertyChanged("TestProperty");
            _testProperty = value;
        }
    }

    public TestablePrivatizableObject(bool privatize) 
        : base(privatize)
    { }

    public TestablePrivatizableObject(PrivatizableObject privatizationController)
        : base(privatizationController) { }
}
 

Comme vous pouvez le voir, l’implémentation n’a plus grand chose de compliqué !

 

A l’utilisation, là aussi rien de bien méchant :

 

Accéder à une propriété :

string value = myObject.TestProperty;

 

Modifier l’état partagé:

using (myObject.Privatize())
{
    myObject.TestProperty = "test";
myObject.TestProperty2 = "test2"; } // a la fin du bloc, l’objet est automatiquement publié

 

Effectuer une série de “reads” atomique :

using (testee.CreateReadRegion())
{
value1 = myObject.TestProperty;
value2 = myObject.TestProperty2; }

 

Cette solution, bien que fournie avec une batterie de tests unitaires, est fournie “as-is”, mais est totalement libre de droits :

Téléchargez la solution