Двойная проверка блокировки словаря "ContainsKey"
Моя команда в настоящее время обсуждает эту проблему.
Код, о котором идет речь, - это что-то вроде строк
if (!myDictionary.ContainsKey(key))
{
lock (_SyncObject)
{
if (!myDictionary.ContainsKey(key))
{
myDictionary.Add(key,value);
}
}
}
Некоторые из сообщений, которые я видел, говорят, что это может быть большой НЕТ НЕТ (при использовании TryGetValue). Тем не менее члены нашей команды говорят, что это нормально, так как "ContainsKey" не выполняет итерацию в коллекции ключей, но проверяет, содержится ли ключ через хэш-код в O (1). Следовательно, они утверждают, что здесь нет никакой опасности.
Я хотел бы получить ваши честные мнения по этой проблеме.
Ответы
Ответ 1
Не делай этого. Это не безопасно.
Вы можете вызывать ContainsKey
из одного потока, а другой поток вызывает Add
. Это просто не поддерживается Dictionary<TKey, TValue>
. Если Add
необходимо перераспределить ведра и т.д., Я могу представить, что вы можете получить некоторые очень странные результаты или исключение. Возможно, это было написано так, что вы не видите никаких неприятных эффектов, но я не хотел бы полагаться на это.
Это одна вещь, использующая двойную проверку блокировки для простого чтения/записи в поле, хотя я бы все же возразил против нее - другой - сделать вызовы API, которые явно описаны как небезопасные для нескольких одновременных вызовов.
Если вы на .NET 4, ConcurrentDictionary
, вероятно, путь вперед. В противном случае просто заблокируйте каждый доступ.
Ответ 2
Если вы находитесь в многопоточной среде, вы можете предпочесть использовать ConcurrentDictionary. Я написал об этом пару месяцев назад, вы можете найти статью полезной: http://colinmackay.co.uk/blog/2011/03/24/parallelisation-in-net-4-0-the-concurrent-dictionary/
Ответ 3
Этот код неверен. Тип Dictionary<TKey, TValue>
не поддерживает одновременные операции чтения и записи. Даже если ваш метод Add
вызывается внутри блокировки, ContainsKey
нет. Следовательно, он легко допускает нарушение одновременного правила чтения/записи и приведет к повреждению вашего экземпляра.
Ответ 4
Он не выглядит потокобезопасным, но, вероятно, было бы трудно заставить его потерпеть неудачу.
Итерационный аргумент поиска хеша не выполняется, например, может быть хеш-столкновение.
Ответ 5
Если этот словарь редко написан и часто читается, я часто использую безопасную двойную блокировку, заменяя весь словарь на запись. Это особенно эффективно, если вы можете выполнять пакетную запись вместе, чтобы сделать их менее частыми.
Например, это сокращенная версия метода, который мы используем, который пытается получить объект схемы, связанный с типом, и если он не может, то он идет вперед и создает объекты схемы для всех типов, которые он находит в той же сборке, что и указанный тип, чтобы свести к минимуму количество копий всего словаря:
public static Schema GetSchema(Type type)
{
if (_schemaLookup.TryGetValue(type, out Schema schema))
return schema;
lock (_syncRoot) {
if (_schemaLookup.TryGetValue(type, out schema))
return schema;
var newLookup = new Dictionary<Type, Schema>(_schemaLookup);
foreach (var t in type.Assembly.GetTypes()) {
var newSchema = new Schema(t);
newLookup.Add(t, newSchema);
}
_schemaLookup = newLookup;
return _schemaLookup[type];
}
}
Таким образом, словарь в этом случае будет перестроен, самое большее, столько раз, сколько есть сборок с типами, которые нуждаются в схемах. Для остальной части жизненного цикла приложения доступ к словарю будет заблокирован. Копия словаря становится единовременной стоимостью инициализации сборки. Словарь swap является потокобезопасным, поскольку записи указателей являются атомарными, поэтому вся ссылка переключается сразу.
Вы можете применять аналогичные принципы и в других ситуациях.