Двойная проверка блокировки словаря " ContainsKey"

Моя группа в настоящее время обсуждает этот вопрос.

рассматриваемый код является чем-то вроде

if (!myDictionary.ContainsKey(key))
{
    lock (_SyncObject)
    {
        if (!myDictionary.ContainsKey(key))
        {
            myDictionary.Add(key,value);
        }
    }
}

некоторые из сообщений, которые я видел, говорят, что это может быть большой NO NO (при использовании TryGetValue). Тем не менее члены нашей команды говорят, что это нормально, так как "ContainsKey" не повторяет коллекцию ключей, но проверяет, содержится ли ключ через хэш-код в O(1). Поэтому они утверждают, что здесь нет никакой опасности.

Я хотел бы получить ваше честное мнение относительно этого вопроса.

5 ответов


не делай этого. Это небезопасно.

вы могли бы называть ContainsKey из одного потока, в то время как другой поток вызывает Add. Это просто не поддерживается Dictionary<TKey, TValue>. Если Add необходимо перераспределить ведра и т. д., Я могу представить вас мог бы получите очень странные результаты или исключение. Это мая были написаны таким образом, что вы не видите никаких неприятных эффектов, но я бы не хотел полагаться на это.

одно дело, используя двойную проверку блокировка для простых операций чтения / записи в поле, хотя я все равно возражаю против этого - это еще один вызов API, который был явно описан как не безопасная для нескольких одновременных вызовов.

если вы находитесь на .NET 4,ConcurrentDictionary вероятно, это путь вперед. В противном случае, просто заблокируйте каждый доступ.


Если вы находитесь в многопоточной среде, вы можете предпочесть использовать ConcurrentDictionary. Я написал об этом в блоге пару месяцев назад, вы можете найти статью полезной: http://colinmackay.co.uk/blog/2011/03/24/parallelisation-in-net-4-0-the-concurrent-dictionary/


этот код некорректен. The Dictionary<TKey, TValue> тип не поддерживает одновременные операции чтения и записи. Даже если ваш Add метод вызывается в замок ContainsKey нет. Следовательно, он легко допускает нарушение правила одновременного чтения / записи и приведет к повреждению в вашем экземпляре


Это не выглядит потокобезопасным, но, вероятно, было бы трудно заставить его потерпеть неудачу.

аргумент итерации против хэш-поиска не выполняется, например, может быть хэш-столкновение.


если этот словарь редко пишется и часто читается, то я часто использую безопасную двойную блокировку, заменяя весь словарь на запись. Это особенно эффективно, если вы можете пакетной записи вместе, чтобы сделать их менее частыми.

например, это сокращенная версия метода, который мы используем, который пытается получить объект схемы, связанный с типом, и если он не может, то он идет вперед и создает объекты схемы для всех типов, которые он находит в той же сборке, что и указанный тип для минимизации количества копий всего словаря:

    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];
        }
    }

таким образом, словарь в этом случае будет перестроен, самое большее, столько раз, сколько есть сборки с типами, которым нужны схемы. В течение оставшейся части жизни приложения доступ к словарю будет свободным от блокировки. Копия словаря становится единовременной стоимостью инициализации сборки. Подкачка словаря потокобезопасна, потому что записи указателя являются атомарными, поэтому вся ссылка получает перешли на один раз.

вы можете применять аналогичные принципы и в других ситуациях.