Возможно ли, чтобы словарь в .Net вызывал блокировку при чтении и записи на нее параллельно?
Я играл с TPL и пытался узнать, как большой беспорядок я мог бы сделать, читая и записывая в тот же словарь параллельно.
Итак, у меня был этот код:
private static void HowCouldARegularDicionaryDeadLock()
{
for (var i = 0; i < 20000; i++)
{
TryToReproduceProblem();
}
}
private static void TryToReproduceProblem()
{
try
{
var dictionary = new Dictionary<int, int>();
Enumerable.Range(0, 1000000)
.ToList()
.AsParallel()
.ForAll(n =>
{
if (!dictionary.ContainsKey(n))
{
dictionary[n] = n; //write
}
var readValue = dictionary[n]; //read
});
}
catch (AggregateException e)
{
e.Flatten()
.InnerExceptions.ToList()
.ForEach(i => Console.WriteLine(i.Message));
}
}
Это было действительно испорчено, было много исключений, в основном из-за ключа не существует, несколько из индекса, не связанного с массивом.
Но после запуска приложения какое-то время он зависает, а процент процессора остается на уровне 25%, машина имеет 8 ядер.
Поэтому я предполагаю, что 2 потока работают на полную мощность.
![введите описание изображения здесь]()
Затем я набросился на него и получил следующее:
![введите описание изображения здесь]()
Он соответствует моему предположению, два потока работают со 100%.
Оба запускают метод FindEntry словаря.
Затем я снова запустил приложение, с dottrace, на этот раз результат несколько отличается:
![введите описание изображения здесь]()
На этот раз один поток запускает FindEntry, другой Insert.
Моя первая интуиция заключалась в том, что он мертв заблокирован, но тогда я думал, что этого не может быть, есть только один общий ресурс, и он не заблокирован.
Итак, как это объяснить?
ps: Я не пытаюсь решить проблему, ее можно исправить с помощью ConcurrentDictionary или путем параллельной агрегации. Я просто ищу разумное объяснение этого.
Ответы
Ответ 1
Похоже, что состояние гонки (а не тупик) - которое, как вы прокомментируете, вызывает испорченное внутреннее состояние.
Словарь не является потокобезопасным, поэтому одновременные чтения и записи в один и тот же контейнер из отдельных потоков (даже если их количество меньше одного) не безопасно.
Как только состояние гонки будет удалено, оно станет undefined, что произойдет; в этом случае то, что кажется бесконечным циклом.
В общем случае, когда требуется доступ на запись, требуется некоторая форма синхронизации.
Ответ 2
Итак, ваш код выполняет Dictionary.FindEntry
. Это не тупик - тупик происходит, когда два потока блокируются таким образом, что заставляет их ждать друг друга, чтобы освободить ресурс, но в вашем случае вы получаете два, казалось бы, бесконечных цикла. Нити не заблокированы.
Посмотрите этот метод в справочном источнике :
private int FindEntry(TKey key) {
if( key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
}
}
return -1;
}
Посмотрите на цикл for
. Часть приращения i = entries[i].next
и угадайте, что: entries
- это поле, которое обновляется в методе Resize
. next
- это поле внутренней Entry
struct:
public int next; // Index of next entry, -1 if last
Если ваш код не может выйти из метода FindEntry
, наиболее вероятной причиной будет то, что вам удалось испортить записи таким образом, что они создают бесконечную последовательность, когда вы будете следовать указателям, указанным next
.
Что касается метода Insert
, он имеет очень похожий цикл for
:
for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next)
Поскольку класс Dictionary
документирован как не-потокобезопасный, вы все равно находитесь в сфере поведения undefined.
Использование ConcurrentDictionary
или шаблона блокировки, такого как ReaderWriterLockSlim
(Dictionary
, является потокобезопасным для одновременных чтений) или простой старый lock
прекрасно решает проблему.