Словарь надежного словаря.

Является ли Dictionary.Add() потокобезопасным при вставке?

У меня есть код, который вставляет ключи из нескольких потоков, мне все еще нужно блокировать словаря Dictionary.Add()

Я получил это исключение при добавлении нового ключа:

Exception Source:    mscorlib
Exception Type: System.IndexOutOfRangeException
Exception Message:   Index was outside the bounds of the array.
Exception Target Site: Insert

Хотя это довольно редко. Я знаю, что Dictionary не является потокобезопасным, хотя я думал, что вызов только .Add не вызовет никаких проблем.

Ответы

Ответ 1

Словарь не является потокобезопасным вообще, независимо от того, добавляете ли вы к нему или нет - внутри него есть несколько внутренних структур, которые необходимо синхронизировать (особенно, когда внутренние hashbuckets меняются).

Вам либо нужно реализовать свою собственную блокировку вокруг любой операции на нем, либо если вы находитесь в .Net 4.0, вы можете использовать новый ConcurrentDictionary, который абсолютно фантастический, и который полностью потокобезопасен.

Другой вариант (обновление)

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

Дайте каждому потоку собственный словарь, который он вставляет.

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

Официальный ответ на вопрос: производительность (после того, как вы приняли)

Так как ваши комментарии говорят, вам нужна идея о лучшем методе (блокировка или слияние) для производительности и т.д. Я не могу сказать вам, что это будет; в конечном итоге его необходимо будет сравнить. Я посмотрю, могу ли я предложить некоторые рекомендации, хотя:)

Во-первых - если у вас есть представление о том, сколько элементов вам потребуется в Dictionar (y/ies), используйте конструктор (int), чтобы свести к минимуму изменение размера.

Операция слияния, вероятно, будет лучше; так как ни один из потоков не будет мешать друг другу. Если процесс, связанный с тем, что два объекта имеют один и тот же ключ, особенно длителен; в этом случае принудительное его выполнение в одном потоке в конце операции может привести к обнулению всех показателей производительности путем параллелизации первого этапа!

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

Если это так, что решение должно быть принято на уровне потока, когда ключ уже присутствует, вам понадобится конструкция lock() {}.

В словаре, это обычно имеет следующую форму:

readonly object locker = new object();
Dictionary<string, IFoo> dictionary = new Dictionary<string, IFoo>();

void threadfunc()
{
  while(work_to_do)
  {
    //get the object outside the lock
    //be optimistic - expect to add; and handle the clash as a 
    //special case
    IFoo nextObj = GetNextObject(); //let say that an IFoo has a .Name
    IFoo existing = null;
    lock(locker)
    {
      //TryGetValue is a god-send for this kind of stuff
      if(!dictionary.TryGetValue(nextObj.Name, out existing))
        dictionary[nextObject.Name] = nextObj;
      else
        MergeOperation(existing, nextObject);
    }
  }
}

Теперь, если этот MergeOperation действительно медленный; то вы можете подумать о том, чтобы освободить блокировку, создав клонированный объект, представляющий слияние существующего и нового объекта, а затем повторно захватив блокировку. Однако вам нужен надежный способ проверки того, что состояние существующего объекта не изменилось между первой блокировкой и второй (для этого полезно использовать номер версии).

Ответ 2

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

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