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 laid. Ma question est la suivante: existe-t-il une meilleure façon, plus élégante, d'implémenter un dictionnaire thread-safe?
Comme l'a dit Peter, vous pouvez encapsuler toute la sécurité des threads à l'intérieur de la classe. Vous devrez être prudent avec tous les événements que vous exposez ou ajoutez, en vous assurant qu'ils sont invoqués en dehors de tous les 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 sûre pour les threads. Cela peut être une raison pour exposer un objet de synchronisation en dehors de votre classe. Une autre façon de procéder 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 (ce qui entraînerait un blocage). L'exposition de l'objet de synchronisation permet au consommateur de prendre ces décisions et ne cache pas l'impasse dans votre classe.
La classe .NET 4.0 qui prend en charge la simultanéité est nommée ConcurrentDictionary
.
Tenter de synchroniser en interne sera presque certainement insuffisant car il est à un niveau d'abstraction trop faible. Supposons que vous rendiez les opérations Add
et ContainsKey
individuellement 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);
}
}
Que se passe-t-il alors 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 méthode Add
lèvera une exception indiquant que la clé existe déjà dans le dictionnaire. Comment cela peut-il être avec un dictionnaire thread-safe, vous 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
et Add
.
Ce qui signifie que pour écrire correctement ce type de scénario, vous avez besoin d'un verrou extérieur le dictionnaire, par ex.
lock (mySafeDictionary)
{
if (!mySafeDictionary.ContainsKey(someKey))
{
mySafeDictionary.Add(someKey, someValue);
}
}
Mais maintenant, étant donné que vous devez écrire du code à verrouillage externe, vous mélangez la synchronisation interne et externe, ce qui entraîne toujours des problèmes tels que le code peu clair et les blocages. Donc, en fin de compte, vous êtes probablement mieux:
Utilisez un Dictionary<TKey, TValue>
et synchroniser en externe, en y incluant les opérations composées, ou
Écrivez un nouveau wrapper thread-safe avec une interface différente (c'est-à-dire pas IDictionary<T>
) qui combine les opérations telles qu'une méthode AddIfNotContained
de sorte que vous n'avez jamais besoin de combiner des opérations à partir de celle-ci.
(J'ai tendance à aller avec # 1 moi-même)
Vous ne devez pas publier votre objet de verrouillage privé via une propriété. L'objet verrou 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.
Il y a plusieurs problèmes avec la méthode d'implémentation que vous décrivez.
Personnellement, j'ai trouvé la meilleure façon d'implémenter une classe thread-safe via l'immuabilité. Cela réduit vraiment le nombre de problèmes que vous pouvez rencontrer avec la sécurité des threads. Consultez Blog d'Eric Lippert pour plus de détails.
Vous n'avez pas besoin de verrouiller la propriété SyncRoot dans vos objets de consommation. Le verrou que vous avez dans les méthodes du dictionnaire est suffisant.
Pour élaborer: Ce qui finit par se produire, c'est que votre dictionnaire est verrouillé pendant une période plus longue que nécessaire.
Ce qui se passe dans votre cas est le suivant:
Disons que le thread A acquiert le verrou sur SyncRoot avant l'appel à m_mySharedDictionary.Add. Le thread 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. À 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 qu'en dehors de l'une des méthodes de dictionnaire si vous deviez effectuer deux opérations sur l'objet dictionnaire qui devaient être garanties pour se produire consécutivement.
Juste une idé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);
}