Возможно ли, чтобы словарь в .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 прекрасно решает проблему.