2009-07-02 7 views
8

J'ai une situation en C# où j'ai une liste de types simples. Cette liste peut être accédée par plusieurs threads: des entrées peuvent être ajoutées ou supprimées, et l'existence d'une entrée peut être vérifiée. J'ai encapsulé la liste dans un objet exposant seulement ces trois opérations jusqu'ici.C# Concurrent List Questions

J'ai quelques cas à gérer (pas exactement les mêmes que les méthodes que je viens de mentionner).
1. Un thread peut simplement vérifier l'existence d'une entrée. (simple)
2. Un thread peut vérifier l'existence d'une entrée et, si elle n'existe pas, l'ajouter.
3. Un thread doit vérifier si une entrée existe et, le cas échéant, attendre qu'il soit supprimé.
4. Une combinaison de 2 et 3, où un thread vérifie l'existence d'une entrée, s'il existe, il doit attendre jusqu'à ce qu'il soit supprimé avant de pouvoir l'ajouter lui-même.

L'idée générale est que l'existence d'une entrée signifie un verrou. Si une entrée existe, l'objet identifié ne peut pas être modifié et le code ne peut pas continuer car il est modifié ailleurs. Cela peut sembler être de simples situations novices mais je me rafraîchis sur les problèmes de simultanéité et cela me rend un peu paranoïaque, et je ne suis pas aussi familier avec les mécanismes de simultanéité de C#.

Quelle serait la meilleure façon de gérer cela? Suis-je totalement éteint? Devrait vérifier et ajouter (tester et définir?) Être combinés dans une quatrième opération atomique? Aurais-je simplement ajouter des blocs de verrouillage à mes méthodes d'accès à la liste?

Aussi, est-il possible de tester ce type de chose (pas les opérations simples, les situations de concurrence)?

Répondre

8

Les tests unitaires seront certainement difficiles.

Cela peut tout simplement se faire avec les mécanismes de concurrence "native" dans .NET: les instructions de verrouillage et Monitor.Wait/Monitor.PulseAll. Cependant, à moins d'avoir un moniteur séparé par article, vous devrez réveiller tous les threads à chaque fois que quelque chose est supprimé, sinon vous ne pourrez pas dire au "bon" thread de se réveiller.

S'il est vraiment juste un mis d'articles, vous pouvez utiliser HashSet<T> au lieu de List<T> pour représenter la collection, en passant - rien que vous avez mentionné est de faire avec la commande.

Exemple de code, en supposant qu'un ensemble est correct pour vous:

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

public class LockCollection<T> 
{ 
    private readonly HashSet<T> items = new HashSet<T>(); 
    private readonly object padlock = new object(); 

    public bool Contains(T item) 
    { 
     lock (padlock) 
     { 
      return items.Contains(item); 
     } 
    } 

    public bool Add(T item) 
    { 
     lock (padlock) 
     { 
      // HashSet<T>.Add does what you want already :) 
      // Note that it will return true if the item 
      // *was* added (i.e. !Contains(item)) 
      return items.Add(item); 
     } 
    } 

    public void WaitForNonExistence(T item) 
    { 
     lock (padlock) 
     { 
      while (items.Contains(item)) 
      { 
       Monitor.Wait(padlock); 
      } 
     } 
    } 

    public void WaitForAndAdd(T item) 
    { 
     lock (padlock) 
     { 
      WaitForNonExistence(item); 
      items.Add(item); 
     } 
    } 

    public void Remove(T item) 
    { 
     lock (padlock) 
     { 
      if (items.Remove(item)) 
      { 
       Monitor.PulseAll(padlock); 
      } 
     } 
    } 
} 

(. Complètement non testé, il est vrai, vous pouvez également spécifier les délais d'attente pour le code d'attente ...)

+0

+1, je suis venu à une solution similaire, mais le HashSet rend plus efficace. Belle démonstration pour la classe Monitor, en utilisant des Locks imbriqués, etc. Seul le problème pourrait être un 'stampede' quand un élément est enlevé. –

8

Bien que # 1 peut être le plus simple à écrire, c'est essentiellement une méthode inutile. À moins que vous ne conserviez le même verrou après avoir terminé une requête pour "existence d'une entrée", vous renvoyez "l'existence d'une entrée à un moment donné dans le passé". Il ne vous donne aucune information sur l'existence actuelle de l'entrée. Entre la découverte d'une valeur dans la liste puis l'opération à récupérer, retirez la valeur, un autre thread pourrait venir et l'enlever pour vous. Les opérations sur une liste simultanée doivent être combinées avec l'opération que vous envisagez de faire en cas d'existence vraie/fausse de cette vérification.Par exemple TestAdd() ou TestRemove() est beaucoup plus sûr que Contient + Ajouter ou Contient + Enlever

+0

Vous devez créer un lien vers votre article de blog sur les interfaces de collecte de threads sécurisées. –

+0

Oui, mais c'est ce que fait le numéro 4. L'utilisabilité des autres méthodes est douteuse, mais pas nécessairement fausse. –

+0

@Henk, je pense # 2- # 4 sont de bonnes méthodes. # 1 n'est pas faux, mais ce n'est pas utile. – JaredPar

1

Il existe un produit pour trouver des conditions de concurrence et autres dans des tests unitaires. C'est ce qu'on appelle TypeMock Racer. Cependant, je ne peux rien dire pour ou contre son efficacité. :)