Блокировка HashSet для параллелизма

При использовании HashSet<string> проверить, был ли предмет обработан ранее (т.е. только Add а также Contains используется). Кроме того, это не имеет значения, когда Contains возвращает false, даже если оно было добавлено ранее...

Я встретил следующее исключение без блокировки:

[IndexOutOfRangeException: индекс находился за пределами массива.] System.Collections.Generic.HashSet`1.AddIfNotPresent(значение T) +6108128

Достаточно ли заблокировать только вызов Add?

Кажется, что следование работает вечно, но это не доказательство...

HashSet<string> hashSet = new HashSet<string>();
Parallel.ForEach(GetString(), h => 
{
    hashSet.Contains(h);
    lock(hashSetLock) 
    {
        hashSet.Add(h); 
    }
    hashSet.Contains(h);
});

Чтобы было точнее: я знаю, что это не потокобезопасный вызов Contains без замка. Мой вопрос (принимая ложные срабатывания), если приведенный выше код может вызвать исключение или может разрушить внутреннее состояние базовой структуры данных (=HashSet).

3 ответа

Решение

Нет, недостаточно только блокировки Add,

Тот факт, что он не дает сбоя, говорит только о том, что он не разбился во время теста.

Вы не можете гарантировать, что:

  • Это не будет терпеть крах в будущем
  • Это даст правильные результаты

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

Вам нужно либо:

  • Блокировка на каждый звонок к нему
  • Используйте многопотоковую структуру данных, созданную для поддержки этого сценария.

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

lock (dLock)
    if (d.ContainsKey("test"))
        return;

var value = ExpensiveCallToObtainValue();
lock (dLock)
    d.Add("test", value);

Между звонком ContainsKey и призыв к Add другой поток, возможно, уже вставил этот ключ.

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

lock (dLock)
{
    if (!d.ContainsKey("test"))
        d.Add("test", ExpensiveCallToObtainValue());
}

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

При использовании HashSet<T>не должно быть ContainsKey проверить, как Add проверит, содержит ли внутренняя коллекция значение или нет:

Возвращаемое значение Тип: System.Boolean

Значение true, если элемент добавлен в объект HashSet; false, если элемент уже присутствует.

Таким образом, вы можете сузить свой код до:

private readonly object syncRoot = new object();
lock (syncRoot)
    hashSet.Add(value);

Какой смысл этих звонков в Contains()? Они ничего не делают. Если вы хотите добавить, только если набор не содержит элемент, вы можете сделать следующее:

if(!hasSet.Contains(h))
{
   lock(hashSetLock)
   {
      if(!hasSet.Contains(h))
      {
         hashSet.Add(h);
      }
   }
}

С этим кодом вы не блокируете, чтобы проверить существующий элемент, но если элемент не был установлен, вы должны проверить снова после блокировки. Что вы получаете? Вы не блокируете, если элемент уже существует.

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