C# Language Anti-Patterns et Gotchas


Exemple

Verrouillage sur une variable allouée / locale

L'une des erreurs lors de l'utilisation de lock est l'utilisation d'objets locaux comme casier dans une fonction. Étant donné que ces instances d'objet local différeront à chaque appel de la fonction, le lock ne fonctionnera pas comme prévu.

List<string> stringList = new List<string>();

public void AddToListNotThreadSafe(string something)
{
    // DO NOT do this, as each call to this method 
    // will lock on a different instance of an Object.
    // This provides no thread safety, it only degrades performance.
    var localLock = new Object();
    lock(localLock)
    {
        stringList.Add(something);
    }
}

// Define object that can be used for thread safety in the AddToList method
readonly object classLock = new object();

public void AddToList(List<string> stringList, string something)
{
    // USE THE classLock instance field to achieve a 
    // thread-safe lock before adding to stringList
    lock(classLock)
    {
        stringList.Add(something);
    }
}

En supposant que le verrouillage restreint l'accès à l'objet de synchronisation lui-même

Si un thread appelle: lock(obj) et un autre thread appelle obj.ToString() deuxième thread ne sera pas bloqué.

object obj = new Object();
 
public void SomeMethod()
{
     lock(obj)
    {
       //do dangerous stuff 
    }
 }

 //Meanwhile on other tread 
 public void SomeOtherMethod()
 {
   var objInString = obj.ToString(); //this does not block
 }

Attend des sous-classes pour savoir quand verrouiller

Parfois, les classes de base sont conçues de telle sorte que leurs sous-classes doivent utiliser un verrou lors de l'accès à certains champs protégés:

public abstract class Base
{
    protected readonly object padlock;
    protected readonly List<string> list;

    public Base()
    {
        this.padlock = new object();
        this.list = new List<string>();
    }

    public abstract void Do();
}

public class Derived1 : Base
{
    public override void Do()
    {
        lock (this.padlock)
        {
            this.list.Add("Derived1");
        }
    }
}

public class Derived2 : Base
{
    public override void Do()
    {
        this.list.Add("Derived2"); // OOPS! I forgot to lock!
    }
}

Il est beaucoup plus sûr d' encapsuler le verrouillage en utilisant une méthode de modèle :

public abstract class Base
{
    private readonly object padlock; // This is now private
    protected readonly List<string> list;

    public Base()
    {
        this.padlock = new object();
        this.list = new List<string>();
    }

    public void Do()
    {
        lock (this.padlock) {
            this.DoInternal();
        }
    }

    protected abstract void DoInternal();
}

public class Derived1 : Base
{
    protected override void DoInternal()
    {
        this.list.Add("Derived1"); // Yay! No need to lock
    }
}

Le verrouillage sur une variable ValueType en boîte ne se synchronise pas

Dans l'exemple suivant, une variable privée est implicitement encadrée car elle est fournie en tant qu'argument d' object à une fonction, s'attendant à ce qu'une ressource de moniteur se verrouille. La boxe se produit juste avant d'appeler la fonction IncInSync, donc l'instance encadrée correspond à un objet de segment différent à chaque appel de la fonction.

public int Count { get; private set; }

private readonly int counterLock = 1;

public void Inc()
{
    IncInSync(counterLock);
}

private void IncInSync(object monitorResource)
{
    lock (monitorResource)
    {
        Count++;
    }
}

La boxe se produit dans la fonction Inc :

BulemicCounter.Inc:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldarg.0     
IL_0003:  ldfld       UserQuery+BulemicCounter.counterLock
IL_0008:  box         System.Int32**
IL_000D:  call        UserQuery+BulemicCounter.IncInSync
IL_0012:  nop         
IL_0013:  ret         

Cela ne signifie pas qu'un ValueType en boîte ne peut pas être utilisé pour le verrouillage du moniteur:

private readonly object counterLock = 1;

Maintenant, la boxe se produit dans le constructeur, ce qui est bien pour le verrouillage:

IL_0001:  ldc.i4.1    
IL_0002:  box         System.Int32
IL_0007:  stfld       UserQuery+BulemicCounter.counterLock

Utiliser des verrous inutilement lorsqu'une alternative plus sûre existe

Un modèle très courant consiste à utiliser une List ou un Dictionary privé dans une classe de thread thread et à verrouiller chaque fois qu'il est accessible:

public class Cache
{
    private readonly object padlock;
    private readonly Dictionary<string, object> values;

    public WordStats()
    {
        this.padlock = new object();
        this.values = new Dictionary<string, object>();
    }
    
    public void Add(string key, object value)
    {
        lock (this.padlock)
        {
            this.values.Add(key, value);
        }
    }

    /* rest of class omitted */
}

S'il y a plusieurs méthodes qui accèdent au dictionnaire de values , le code peut devenir très long et, plus important encore, le verrouillage tout le temps en cache l' intention . Le verrouillage est également très facile à oublier et le manque de verrouillage peut rendre très difficile la recherche de bogues.

En utilisant un ConcurrentDictionary , nous pouvons éviter de verrouiller complètement:

public class Cache
{
    private readonly ConcurrentDictionary<string, object> values;

    public WordStats()
    {
        this.values = new ConcurrentDictionary<string, object>();
    }
    
    public void Add(string key, object value)
    {
        this.values.Add(key, value);
    }

    /* rest of class omitted */
}

L'utilisation de collections simultanées améliore également les performances car toutes utilisent des techniques sans verrouillage dans une certaine mesure.