Quelle est la meilleure façon d'implémenter un dictionnaire thread-safe?

109

J'ai pu implémenter un dictionnaire thread-safe en C # en dérivant d'IDictionary et en définissant un objet SyncRoot privé:

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public object SyncRoot
    {
        get { return syncRoot; }
    } 

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
    }

    // more IDictionary members...
}

Je verrouille ensuite cet objet SyncRoot dans tous mes consommateurs (plusieurs threads):

Exemple:

lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}

J'ai pu le faire fonctionner, mais cela a abouti à un code horrible. Ma question est la suivante: y a-t-il une manière meilleure et plus élégante d'implémenter un dictionnaire thread-safe?

GP.
la source
3
Qu'est-ce que tu trouves laid à ce sujet?
Asaf R
1
Je pense qu'il fait référence à toutes les instructions de verrouillage qu'il a tout au long de son code dans les consommateurs de la classe SharedDictionary - il verrouille le code appelant chaque fois qu'il accède à une méthode sur un objet SharedDictionary.
Peter Meyer
Au lieu d'utiliser la méthode Add, essayez de le faire en attribuant des valeurs ex-m_MySharedDictionary ["key1"] = "item1", c'est thread-safe.
testuser

Réponses:

43

Comme Peter l'a dit, vous pouvez encapsuler toute la sécurité des threads dans la classe. Vous devrez faire attention à tous les événements que vous exposez ou ajoutez, en vous assurant qu'ils sont invoqués en dehors des verrous.

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
        OnItemAdded(EventArgs.Empty);
    }

    public event EventHandler ItemAdded;

    protected virtual void OnItemAdded(EventArgs e)
    {
        EventHandler handler = ItemAdded;
        if (handler != null)
            handler(this, e);
    }

    // more IDictionary members...
}

Edit: Les documents MSDN soulignent que l'énumération n'est pas intrinsèquement thread-safe. Cela peut être une raison pour exposer un objet de synchronisation en dehors de votre classe. Une autre façon d'aborder cela serait de fournir des méthodes pour effectuer une action sur tous les membres et de verrouiller l'énumération des membres. Le problème avec ceci est que vous ne savez pas si l'action passée à cette fonction appelle un membre de votre dictionnaire (cela entraînerait un blocage). L'exposition de l'objet de synchronisation permet au consommateur de prendre ces décisions et ne cache pas le blocage dans votre classe.

fryguybob
la source
@fryguybob: L'énumération était en fait la seule raison pour laquelle j'exposais l'objet de synchronisation. Par convention, j'effectuerais un verrou sur cet objet uniquement lorsque j'énumérerais via la collection.
GP.
1
Si votre dictionnaire n'est pas trop volumineux, vous pouvez l'énumérer sur une copie et l'inclure dans la classe.
fryguybob
2
Mon dictionnaire n'est pas trop volumineux, et je pense que cela a fait l'affaire. Ce que j'ai fait, c'est créer une nouvelle méthode publique appelée CopyForEnum () qui retourne une nouvelle instance d'un dictionnaire avec des copies du dictionnaire privé. Cette méthode a ensuite été appelée pour les énumérations et le SyncRoot a été supprimé. Merci!
GP.
12
Ce n'est pas non plus une classe intrinsèquement threadsafe puisque les opérations de dictionnaire ont tendance à être granulaires. Un peu de logique dans le sens de if (dict.Contains (what)) {dict.Remove (peu importe); dict.Add (peu importe, newval); } est assurément une condition de concurrence qui attend d'arriver.
plinthe
207

La classe .NET 4.0 qui prend en charge l'accès concurrentiel est nommée ConcurrentDictionary.

Hector Correa
la source
4
Veuillez marquer ceci comme réponse, vous n'avez pas besoin de dictionnaire personnalisé si le propre .Net a une solution
Alberto León
27
(Rappelez-vous que l'autre réponse a été écrite bien avant l'existence de .NET 4.0 (sortie en 2010).)
Jeppe Stig Nielsen
1
Malheureusement, ce n'est pas une solution sans verrouillage, elle est donc inutile dans les assemblys sécurisés SQL Server CLR. Vous auriez besoin de quelque chose comme ce qui est décrit ici: cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf ou peut-être cette implémentation: github.com/hackcraft/Ariadne
Triynko
2
Vraiment vieux, je sais, mais il est important de noter que l'utilisation du ConcurrentDictionary par rapport à un dictionnaire peut entraîner des pertes de performances importantes. Ceci est probablement le résultat d'un changement de contexte coûteux, alors assurez-vous d'avoir besoin d'un dictionnaire thread-safe avant d'en utiliser un.
consanguine
60

Tenter de synchroniser en interne sera presque certainement insuffisant car c'est à un niveau d'abstraction trop bas. Disons que vous rendez les opérations Addet ContainsKeyindividuellement thread-safe comme suit:

public void Add(TKey key, TValue value)
{
    lock (this.syncRoot)
    {
        this.innerDictionary.Add(key, value);
    }
}

public bool ContainsKey(TKey key)
{
    lock (this.syncRoot)
    {
        return this.innerDictionary.ContainsKey(key);
    }
}

Alors que se passe-t-il lorsque vous appelez ce bit de code censé être thread-safe à partir de plusieurs threads? Cela fonctionnera-t-il toujours bien?

if (!mySafeDictionary.ContainsKey(someKey))
{
    mySafeDictionary.Add(someKey, someValue);
}

La réponse simple est non. À un moment donné, la Addméthode lèvera une exception indiquant que la clé existe déjà dans le dictionnaire. Comment cela peut-il être avec un dictionnaire thread-safe, demandez-vous? Eh bien, juste parce que chaque opération est thread-safe, la combinaison de deux opérations ne l'est pas, car un autre thread pourrait la modifier entre votre appel à ContainsKeyetAdd .

Ce qui signifie pour écrire correctement ce type de scénario, vous avez besoin d'un verrou en dehors du dictionnaire, par exemple

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}

Mais maintenant, étant donné que vous devez écrire du code de verrouillage externe, vous mélangez la synchronisation interne et externe, ce qui conduit toujours à des problèmes tels que du code peu clair et des blocages. Donc, en fin de compte, vous êtes probablement mieux:

  1. Utilisez une normale Dictionary<TKey, TValue>et synchronisez en externe, en y englobant les opérations composées, ou

  2. Ecrivez un nouveau wrapper thread-safe avec une interface différente (c'est-à-dire pas IDictionary<T>) qui combine les opérations telles qu'une AddIfNotContainedméthode afin que vous n'ayez jamais besoin de combiner des opérations à partir de celle-ci.

(J'ai tendance à aller avec # 1 moi-même)

Greg Beech
la source
10
Il vaut la peine de souligner que .NET 4.0 inclura tout un tas de conteneurs thread-safe tels que des collections et des dictionnaires, qui ont une interface différente de la collection standard (c'est-à-dire qu'ils font l'option 2 ci-dessus pour vous).
Greg Beech
1
Il convient également de noter que la granularité offerte par un verrouillage, même grossier, sera souvent suffisante pour une approche à plusieurs lecteurs à un seul écrivain si l'on conçoit un énumérateur approprié qui permet à la classe sous-jacente de savoir quand il est supprimé (méthodes qui voudraient écrire le dictionnaire tout en un énumérateur non disposé existe devrait remplacer le dictionnaire par une copie).
supercat
6

Vous ne devez pas publier votre objet de verrouillage privé via une propriété. L'objet de verrouillage doit exister en privé dans le seul but de servir de point de rendez-vous.

Si les performances s'avèrent médiocres avec le verrou standard, la collection de verrous Power Threading de Wintellect peut être très utile.

Jonathan Webb
la source
5

Il y a plusieurs problèmes avec la méthode de mise en œuvre que vous décrivez.

  1. Vous ne devriez jamais exposer votre objet de synchronisation. Cela vous ouvrira à un consommateur qui saisit l'objet et le verrouille, puis vous portez un toast.
  2. Vous implémentez une interface non thread-safe avec une classe thread-safe. IMHO cela vous coûtera sur la route

Personnellement, j'ai trouvé que le meilleur moyen d'implémenter une classe thread-safe est via l'immuabilité. Cela réduit vraiment le nombre de problèmes que vous pouvez rencontrer avec la sécurité des threads. Consultez le blog d'Eric Lippert pour plus de détails.

JaredPar
la source
3

Vous n'avez pas besoin de verrouiller la propriété SyncRoot dans vos objets consommateur. Le verrou que vous avez dans les méthodes du dictionnaire est suffisant.

Élaborer: Ce qui finit par se produire, c'est que votre dictionnaire est verrouillé pendant une période plus longue que nécessaire.

Voici ce qui se passe dans votre cas:

Dites que le thread A acquiert le verrou sur SyncRoot avant l'appel à m_mySharedDictionary.Add. Le fil B tente alors d'acquérir le verrou mais est bloqué. En fait, tous les autres threads sont bloqués. Le thread A est autorisé à appeler la méthode Add. Au niveau de l'instruction de verrouillage dans la méthode Add, le thread A est autorisé à obtenir à nouveau le verrou car il le possède déjà. En quittant le contexte de verrouillage dans la méthode, puis en dehors de la méthode, le thread A a libéré tous les verrous permettant aux autres threads de continuer.

Vous pouvez simplement autoriser n'importe quel consommateur à appeler la méthode Add car l'instruction de verrouillage dans votre méthode Add de classe SharedDictionary aura le même effet. À ce stade, vous disposez d'un verrouillage redondant. Vous ne verrouilleriez SyncRoot en dehors de l'une des méthodes de dictionnaire que si vous deviez effectuer deux opérations sur l'objet de dictionnaire dont il fallait garantir qu'elles se produisent consécutivement.

Peter Meyer
la source
1
Ce n'est pas vrai ... si vous effectuez deux opérations l'une après l'autre qui sont en interne thread-safe, cela ne signifie pas que le bloc de code global est thread-safe. Par exemple: if (! MyDict.ContainsKey (someKey)) {myDict.Add (someKey, someValue); } ne serait pas threadsafe, même si ContainsKey et Add sont threadsafe
Tim
Votre argument est correct, mais est hors de contexte avec ma réponse et la question. Si vous regardez la question, elle ne parle pas d'appeler ContainsKey, ni ma réponse. Ma réponse fait référence à l'acquisition d'un verrou sur SyncRoot qui est montré dans l'exemple de la question d'origine. Dans le contexte de l'instruction lock, une ou plusieurs opérations thread-safe s'exécuteraient en effet en toute sécurité.
Peter Meyer
Je suppose que si tout ce qu'il fait est d'ajouter au dictionnaire, mais comme il a "// plus de membres IDictionary ...", je suppose qu'à un moment donné, il voudra également relire les données du dictionnaire. Si tel est le cas, il doit y avoir un mécanisme de verrouillage accessible de l'extérieur. Peu importe qu'il s'agisse du SyncRoot dans le dictionnaire lui-même ou d'un autre objet utilisé uniquement pour le verrouillage, mais sans un tel schéma, le code global ne sera pas thread-safe.
Tim
Le mécanisme de verrouillage externe est comme il le montre dans son exemple dans la question: lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (...); } - il serait parfaitement sûr de faire ce qui suit: lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (...)) {m_MySharedDictionary.Add (...); }} En d'autres termes, le mécanisme de verrouillage externe est l'instruction de verrouillage qui opère sur la propriété publique SyncRoot.
Peter Meyer
0

Juste une pensée pourquoi ne pas recréer le dictionnaire? Si la lecture est une multitude d'écritures, le verrouillage synchronisera toutes les requêtes.

exemple

    private static readonly object Lock = new object();
    private static Dictionary<string, string> _dict = new Dictionary<string, string>();

    private string Fetch(string key)
    {
        lock (Lock)
        {
            string returnValue;
            if (_dict.TryGetValue(key, out returnValue))
                return returnValue;

            returnValue = "find the new value";
            _dict = new Dictionary<string, string>(_dict) { { key, returnValue } };

            return returnValue;
        }
    }

    public string GetValue(key)
    {
        string returnValue;

        return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
    }
verbedr
la source
-5

Collections et synchronisation

MagicKat
la source
8
-1 J'ai voté contre parce que a) c'est juste un lien sans explication et b) c'est juste un lien sans explication!
El Ronnoco