Каков наилучший способ реализации многопоточного словаря?
Я смог реализовать поточно-безопасный словарь в C#, унаследовав от IDictionary и определив частный объект SyncRoot:
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...
}
Затем я блокирую этот объект SyncRoot для всех моих потребителей (несколько потоков):
Пример:
lock (m_MySharedDictionary.SyncRoot)
{
m_MySharedDictionary.Add(...);
}
Я смог заставить это работать, но это привело к некоторому уродливому коду. У меня вопрос, есть ли лучший, более элегантный способ реализации многопоточного словаря?
8 ответов
Как сказал Питер, вы можете инкапсулировать всю безопасность потоков внутри класса. Вам нужно быть осторожным с любыми событиями, которые вы выставляете или добавляете, следя за тем, чтобы они вызывались вне любых блокировок.
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...
}
Редактировать: Документы MSDN указывают, что перечисление по своей природе не является потокобезопасным. Это может быть одной из причин выставления объекта синхронизации за пределы вашего класса. Другой способ приблизиться к этому - предоставить некоторые методы для выполнения действий над всеми членами и ограничить перечисление членов. Проблема в том, что вы не знаете, вызывает ли действие, переданное этой функции, некоторый член вашего словаря (что приведет к тупику). Предоставление объекта синхронизации позволяет потребителю принимать эти решения и не скрывает тупик внутри вашего класса.
Класс.NET 4.0, поддерживающий параллелизм, называется ConcurrentDictionary
,
Попытки внутренней синхронизации почти наверняка будут недостаточными, поскольку уровень абстракции слишком низок. Скажи, что ты делаешь Add
а также ContainsKey
Операции индивидуально поточнобезопасны следующим образом:
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);
}
}
Что происходит, когда вы вызываете этот предположительно поточно-ориентированный бит кода из нескольких потоков? Это всегда будет работать нормально?
if (!mySafeDictionary.ContainsKey(someKey))
{
mySafeDictionary.Add(someKey, someValue);
}
Простой ответ - нет. В какой-то момент Add
Метод выдаст исключение, указывающее, что ключ уже существует в словаре. Вы можете спросить, как это может быть с потокобезопасным словарем? Ну, просто потому, что каждая операция является поточно-безопасной, комбинация двух операций не является, так как другой поток может изменить ее между вашими вызовами ContainsKey
а также Add
,
Это означает, что для правильного написания сценария этого типа вам нужна блокировка вне словаря, например
lock (mySafeDictionary)
{
if (!mySafeDictionary.ContainsKey(someKey))
{
mySafeDictionary.Add(someKey, someValue);
}
}
Но теперь, поскольку вам приходится писать код с внешней блокировкой, вы смешиваете внутреннюю и внешнюю синхронизацию, что всегда приводит к таким проблемам, как нечеткий код и взаимоблокировки. Так что в конечном итоге вы, вероятно, лучше либо:
Используйте нормальный
Dictionary<TKey, TValue>
и синхронизировать внешне, заключив в него составные операции, илиНаписать новую поточно-ориентированную оболочку с другим интерфейсом (т.е.
IDictionary<T>
), который сочетает в себе такие операции, какAddIfNotContained
метод, поэтому вам никогда не нужно комбинировать операции из него.
(Я склонен идти с #1 сам)
Вы не должны публиковать свой частный объект блокировки через свойство. Объект блокировки должен существовать в частном порядке с единственной целью действовать как точка свидания.
Если при использовании стандартной блокировки производительность оказывается низкой, то набор блокировок Win Threadlect Power Threading может быть очень полезным.
Есть несколько проблем с методом реализации, который вы описываете.
- Вы никогда не должны выставлять свой объект синхронизации. Сделав это, вы откроете себя для потребителя, который схватит объект и зафиксирует его, и тогда вы будете тостом.
- Вы реализуете потокобезопасный интерфейс с потокобезопасным классом. ИМХО это будет стоить вам в будущем
Лично я нашел лучший способ реализовать потокобезопасный класс с помощью неизменяемости. Это действительно уменьшает количество проблем, с которыми вы можете столкнуться при безопасности потоков. Проверьте блог Эрика Липперта для более подробной информации.
Вам не нужно блокировать свойство SyncRoot в ваших потребительских объектах. Блокировка, которую вы используете в методах словаря, достаточна.
Для разработки: в конечном итоге ваш словарь заблокирован на более длительный период времени, чем необходимо.
В вашем случае происходит следующее:
Скажем, поток A получает блокировку SyncRoot перед вызовом m_mySharedDictionary.Add. Затем поток B пытается получить блокировку, но блокируется. На самом деле все остальные темы заблокированы. Поток A может вызывать метод Add. В операторе lock в методе Add потоку A снова разрешено получить блокировку, поскольку он уже имеет ее. После выхода из контекста блокировки в методе, а затем вне метода поток A снял все блокировки, позволив другим потокам продолжить работу.
Вы можете просто разрешить любому потребителю вызывать метод Add, поскольку оператор блокировки внутри вашего метода Add класса SharedDictionary будет иметь такой же эффект. На данный момент у вас есть избыточная блокировка. Вы можете заблокировать SyncRoot вне одного из методов словаря, если бы вам пришлось выполнить две операции над объектом словаря, которые должны были гарантированно выполняться последовательно.
Просто мысль, почему бы не воссоздать словарь? Если чтение - это множество записей, блокировка синхронизирует все запросы.
пример
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);
}