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?
Réponses:
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.
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.
la source
La classe .NET 4.0 qui prend en charge l'accès concurrentiel est nommée
ConcurrentDictionary
.la source
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
Add
etContainsKey
individuellement thread-safe comme suit: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?
La réponse simple est non. À un moment donné, la
Add
mé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 àContainsKey
etAdd
.Ce qui signifie pour écrire correctement ce type de scénario, vous avez besoin d'un verrou en dehors du dictionnaire, par exemple
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:
Utilisez une normale
Dictionary<TKey, TValue>
et synchronisez en externe, en y englobant les opérations composées, ouEcrivez un nouveau wrapper thread-safe avec une interface différente (c'est-à-dire pas
IDictionary<T>
) qui combine les opérations telles qu'uneAddIfNotContained
mé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)
la source
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.
la source
Il y a plusieurs problèmes avec la méthode de mise en œuvre que vous décrivez.
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.
la source
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.
la source
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
la source
Collections et synchronisation
la source