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

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

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

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

Некоторые из постов, которые я видел, говорят, что это может быть большим НЕТ НЕТ (при использовании 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/

Этот код неверен. 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];
        }
    }

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

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

Другие вопросы по тегам