Не думаю, что вам нужно , чтобы доказать это, вам просто нужно отослать людей к документации для Dictionary<TKey, TValue>
:
Словарь может поддерживать несколько читателей одновременно, , пока коллекция не изменена. Несмотря на это, перечисление через коллекцию по сути не является потокобезопасной процедурой. В редкий случай, когда перечисление конкурирует с доступом для записи, коллекция должна быть заблокирована в течение всего перечисления. Чтобы разрешить доступ к коллекции из нескольких потоков для чтения и записи, необходимо реализовать собственную синхронизацию.
На самом деле это общеизвестный факт (или должен быть), что вы не можете читать из словаря, пока другой поток пишет в него. Я видел несколько вопросов типа «причудливая многопоточность» здесь, на SO, где выяснилось, что автор не осознавал, что это небезопасно.
Проблема не связана конкретно с двойной проверкой блокировки, просто словарь не является потокобезопасным классом даже для сценария с одним записывающим устройством или одним читателем.
Я сделаю еще один шаг и покажу вам, почему в Reflector это не обеспечивает многопоточность:
private int FindEntry(TKey key)
{
// Snip a bunch of code
for (int i = this.buckets[num % this.buckets.Length]; i >= 0;
i = this.entries[i].next)
// Snip a bunch more code
}
private void Resize()
{
int prime = HashHelpers.GetPrime(this.count * 2);
int[] numArray = new int[prime];
// Snip a whole lot of code
this.buckets = numArray;
}
Посмотрите, что может произойти, если метод Resize
работает, когда хотя бы один читатель вызывает FindEntry
:
- Поток A: добавляет элемент, что приводит к динамическому изменению размера;
- Поток B: вычисляет смещение сегмента как (хэш-код% количества сегментов);
- Тема A: изменяет ведра на другой (простой) размер;
- Поток B: выбирает индекс элемента из нового массива сегмента в старом индексе сегмента;
- Указатель нити B. больше не действителен.
И это именно то, что терпит неудачу в примере с dtb. Поток A ищет в словаре ключ, заранее известный как , но пока он не найден. Зачем? Поскольку метод FindValue
выбрал то, что он считал правильным сегментом, но прежде чем он даже имел возможность заглянуть внутрь, поток B изменил сегменты, и теперь поток A ищет какой-то совершенно случайный сегмент, который не содержит или даже не ведет справа от входа.
Мораль истории: TryGetValue
не является атомарной операцией, а Dictionary<TKey, TValue>
не является потокобезопасным классом. Вам нужно беспокоиться не только о параллельных записях; вы не можете одновременно выполнять чтение и запись.
На самом деле проблема на самом деле намного глубже, из-за переупорядочения команд с помощью джиттера и процессора, устаревших кэшей и т. Д. - здесь нет никаких барьеров памяти - но это должно доказать за пределами сомневаюсь, , что существует явное состояние гонки, если у вас Add
вызов запущен одновременно с TryGetValue
вызовом.