.NET - блокировка словаря против ConcurrentDictionary

Я не смог найти достаточно информации о ConcurrentDictionary типах, поэтому решил спросить об этом здесь.

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

Недавно я узнал, что в .NET 4.0 есть набор поточно-ориентированных коллекций, и это кажется мне очень приятным. Мне было интересно, какой вариант будет «более эффективным и простым в управлении», поскольку у меня есть выбор между наличием обычного Dictionary с синхронизированным доступом или ConcurrentDictionary, который уже является потокобезопасным.

Ссылка на .NET 4.0 ConcurrentDictionary


person TheAJ    schedule 22.12.2009    source источник


Ответы (7)


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

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

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

А теперь подумайте об этом. Что, если в магазине с одним продавцом вы дойдете до конца очереди и спросите продавца: «У вас есть туалетная бумага?», И он скажет «Да», а затем вы скажете: «Хорошо, я» Я свяжусь с вами, когда узнаю, сколько мне нужно », тогда, когда вы снова окажетесь в очереди, магазин, конечно, можно будет распродать. Этот сценарий не предотвращается потокобезопасной коллекцией.

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

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

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

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

вы можете получить исключение NullReferenceException, потому что между tree.Count и tree.First() другой поток очистил оставшиеся узлы в дереве, что означает, что First() вернет null.

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

person Lasse V. Karlsen    schedule 27.12.2009
comment
Каждый раз, когда я читаю эту статью, хотя все это правда, мы как-то идем по ложному пути. - person scope_creep; 09.09.2010
comment
Основная проблема в приложении с поддержкой потоков - изменяемые структуры данных. Если вы сможете этого избежать, вы избавите себя от кучи неприятностей. - person Lasse V. Karlsen; 09.09.2010
comment
Очевидно, словари могут иметь некоторые действительно неприятные побочные эффекты потоковой передачи: stackoverflow.com/questions/14838032/ - person jocull; 26.02.2013
comment
@ LasseV.Karlsen: Использование потоков обычно имеет ограниченную полезность при полном отсутствии общего изменяемого состояния, если только все, что делает поток, не может быть полностью описано его возвращаемым значением. Ключ к созданию многопоточности заключается не в том, чтобы сделать все неизменяемым, а в том, чтобы каждый разделяемый объект имел либо неизменяемое состояние, либо неизменяемую идентичность. Если один поток изменяет идентичность объекта, единственный способ узнать об этом другой поток - это если его идентичность инкапсулирована в состоянии некоторого другого объекта, идентичность которого неизменна. - person supercat; 26.04.2013
comment
Это хорошее объяснение безопасности потоков, однако я не могу не чувствовать, что это на самом деле не решает вопрос OP. То, что спросил OP (и то, что я впоследствии нашел в поисках этого вопроса), было разницей между использованием стандартного Dictionary и самостоятельной обработкой блокировки по сравнению с использованием типа ConcurrentDictionary, встроенного в .NET 4+. На самом деле я немного сбит с толку, что это было принято. - person mclark1129; 29.04.2013
comment
Безопасность потоков - это больше, чем просто использование правильной коллекции. Использование правильной коллекции - это начало, а не единственное, с чем вам придется иметь дело. Я предполагаю, что это то, что хотел знать ОП. Я не могу догадаться, почему это было принято, прошло много времени с тех пор, как я это написал. - person Lasse V. Karlsen; 29.04.2013
comment
Я думаю, что @ LasseV.Karlsen пытается сказать, что ... безопасности потоков легко достичь, но вы должны обеспечить определенный уровень параллелизма в том, как вы справляетесь с этой безопасностью. Спросите себя: могу ли я выполнять больше операций одновременно, сохраняя потокобезопасность объекта? Рассмотрим следующий пример: два покупателя хотят вернуть разные товары. Когда вы, как менеджер, совершаете поездку, чтобы пополнить запасы своего магазина, можете ли вы просто пополнить запасы обоих товаров за одну поездку и при этом обрабатывать запросы обоих клиентов, или вы собираетесь совершать поездку каждый раз, когда покупатель возвращает товар? - person Alexandru; 06.02.2014

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

Например, если вы сначала проверяете, существует ли ключ, а затем получаете значение, соответствующее этому ключу, этот ключ может больше не существовать даже с версией ConcurrentDictionary (потому что другой поток мог удалить ключ). В этом случае вам все равно нужно использовать блокировку (или лучше: объедините два вызова, используя TryGetValue).

Так что используйте их, но не думайте, что это дает вам возможность игнорировать все проблемы параллелизма. Тебе по-прежнему нужно быть осторожным.

person Mark Byers    schedule 22.12.2009
comment
То есть вы говорите, что если вы неправильно используете объект, он не сработает? - person ChaosPandion; 23.12.2009
comment
Это почему? Коллекция как-то теряет сдачу? - person Bruno Brant; 23.12.2009
comment
В основном вам нужно использовать TryAdd вместо обычного Contains - ›Add. - person ChaosPandion; 23.12.2009
comment
@Bruno: Нет. Если вы не заблокировали, другой поток мог удалить ключ между двумя вызовами. - person Mark Byers; 23.12.2009
comment
Не хочу показаться здесь кратким, но TryGetValue всегда был частью Dictionary и всегда был рекомендуемым подходом. Важными параллельными методами, которые вводит ConcurrentDictionary, являются AddOrUpdate и GetOrAdd. Итак, хороший ответ, но можно было бы выбрать лучший пример. - person Aaronaught; 27.12.2009
comment
Правильно - в отличие от базы данных, в которой есть транзакция, в большинстве параллельных структур поиска в памяти отсутствует концепция изоляции, они гарантируют только правильность внутренней структуры данных. - person Chang; 08.05.2012
comment
@all Я добавляю ключ в параллельный словарь, и нет никакого смысла удалять ключи вообще. Итак, я использую tryGet и tryAdd? Может ли это вызвать проблемы в будущем? - person kbvishnu; 23.10.2012
comment
был бы хорош любой пример - person T.Todua; 04.06.2020

Внутри ConcurrentDictionary использует отдельную блокировку для каждого хеш-ведра. Пока вы используете только Add / TryGetValue и подобные методы, которые работают с отдельными записями, словарь будет работать как почти свободная от блокировки структура данных с соответствующим приятным преимуществом в производительности. OTOH методы перечисления (включая свойство Count) блокируют сразу все сегменты и поэтому хуже, чем синхронизированный словарь, с точки зрения производительности.

Я бы сказал, просто используйте ConcurrentDictionary.

person Stefan Dragnev    schedule 26.12.2010

Я думаю, что метод ConcurrentDictionary.GetOrAdd - это именно то, что нужно большинству многопоточных сценариев.

person Konstantin    schedule 27.12.2009
comment
Я бы также использовал TryGet, иначе вы тратите усилия на инициализацию второго параметра для GetOrAdd каждый раз, когда ключ уже существует. - person Jorrit Schippers; 10.12.2013
comment
GetOrAdd имеет перегрузку GetOrAdd (TKey, Func ‹TKey, TValue›), поэтому второй параметр можно лениво инициализировать только в том случае, если ключ не существует в Словарь. Кроме того, TryGetValue используется для получения данных, а не для изменения. Последующие вызовы каждого из этих методов могут привести к тому, что другой поток мог добавить или удалить ключ между двумя вызовами. - person sgnsajgon; 11.10.2014
comment
Это должен быть отмеченный ответ на вопрос, даже если сообщение Лассе отличное. - person Spivonious; 26.10.2016

Вы видели реактивные расширения для .Net 3.5sp1? По словам Джона Скита, они сделали резервную копию пакета параллельных расширений и параллельных структур данных для .Net3.5 sp1.

Существует набор примеров для .Net 4 Beta 2, в котором довольно подробно описано, как использовать их параллельные расширения.

Я только что провел последнюю неделю, тестируя ConcurrentDictionary, используя 32 потока для выполнения ввода-вывода. Кажется, что он работает так, как рекламируется, что указывает на то, что в него было вложено огромное количество испытаний.

Изменить: .NET 4 ConcurrentDictionary и шаблоны.

Microsoft выпустила PDF-файл под названием «Шаблоны параллельного программирования». Его действительно стоит загрузить, поскольку в нем очень подробно описаны правильные шаблоны для использования параллельных расширений .Net 4 и анти-шаблоны, которых следует избегать. Вот он.

person scope_creep    schedule 05.01.2010

По сути, вы хотите использовать новый ConcurrentDictionary. Сразу после установки вам нужно писать меньше кода, чтобы сделать программы потокобезопасными.

person ChaosPandion    schedule 22.12.2009
comment
есть ли проблема, если я продолжу использовать Concurrent Dictionery? - person kbvishnu; 23.10.2012

Мы использовали ConcurrentDictionary для кэшированной коллекции, которая повторно заполняется каждые 1 час, а затем читается несколькими клиентскими потоками, аналогично решению для вопроса Является ли этот пример потокобезопасным?.

Мы обнаружили, что изменение его на ReadOnlyDictionary улучшило общую производительность.

person Michael Freidgeim    schedule 05.05.2013
comment
ReadOnlyDictionary - это просто оболочка для другого IDictionary. Что вы используете под капотом? - person Lu55; 05.10.2016
comment
@ Lu55, мы использовали библиотеку MS .Net и выбирали среди доступных / применимых словарей. Я не знаю о внутренней реализации ReadOnlyDictionary в MS - person Michael Freidgeim; 05.10.2018