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

131

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

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

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

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

TheAJ
источник
3
Возможно, вы захотите увидеть эту статью проекта кода на эту тему
nawfal

Ответы:

146

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

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

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

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

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

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

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

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

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

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

Лассе В. Карлсен
источник
9
Каждый раз, когда я читаю эту статью, хотя все это правда, мы как-то идем по ложному пути.
scope_creep
19
Основная проблема в приложении с поддержкой потоков - это изменяемые структуры данных. Если вы сможете этого избежать, вы избавите себя от множества проблем.
Лассе В. Карлсен,
89
Это хорошее объяснение безопасности потоков, однако я не могу не чувствовать, что это на самом деле не решает вопрос OP. То, что спросил OP (и то, что я впоследствии наткнулся на этот вопрос), было разницей между использованием стандарта Dictionaryи обработкой блокировки самостоятельно по сравнению с использованием ConcurrentDictionaryтипа, встроенного в .NET 4+. На самом деле я немного озадачен, что это было принято.
mclark1129
4
Безопасность потоков - это больше, чем просто использование правильной коллекции. Использование правильной коллекции - это начало, а не единственное, с чем вам придется иметь дело. Я думаю, это то, что хотел знать ОП. Я не могу догадаться, почему это было принято, я давно не писал это.
Лассе В. Карлсен
2
Я думаю, что @ LasseV.Karlsen пытается сказать, что ... безопасности потоков легко достичь, но вы должны обеспечить определенный уровень параллелизма в том, как вы справляетесь с этой безопасностью. Спросите себя: «Могу ли я выполнять больше операций одновременно, сохраняя потокобезопасность объекта?» Рассмотрим следующий пример: два клиента хотят вернуть разные товары. Когда вы, как менеджер, совершаете поездку, чтобы пополнить запасы своего магазина, можете ли вы просто пополнить запасы обоих товаров за одну поездку и по-прежнему обрабатывать запросы обоих клиентов, или вы собираетесь совершать поездку каждый раз, когда покупатель возвращает товар?
Alexandru
68

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

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

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

Марк Байерс
источник
4
В общем, вы говорите, что если вы не используете объект правильно, он не будет работать?
ChaosPandion
3
В основном вам нужно использовать TryAdd вместо обычного Contains -> Add.
ChaosPandion,
3
@Bruno: Нет. Если вы не заблокировали, другой поток мог удалить ключ между двумя вызовами.
Марк Байерс,
13
Не хочу показаться здесь кратким, но TryGetValueвсегда был частью Dictionaryи всегда был рекомендуемым подходом. Важными "параллельными" методами, которые ConcurrentDictionaryвводят, являются AddOrUpdateи GetOrAdd. Итак, хороший ответ, но можно было бы выбрать лучший пример.
Aaronaught
4
Правильно - в отличие от базы данных, в которой есть транзакция, в большинстве параллельных структур поиска в памяти отсутствует концепция изоляции , они гарантируют только правильность внутренней структуры данных.
Чанг
39

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

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

Стефан Драгнев
источник
15

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

Константин
источник
1
Я бы также использовал TryGet, иначе вы тратите усилия на инициализацию второго параметра для GetOrAdd каждый раз, когда ключ уже существует.
Йоррит Шипперс
7
GetOrAdd имеет перегрузку GetOrAdd (TKey, Func <TKey, TValue>) , поэтому второй параметр может быть лениво инициализирован только в том случае, если ключ не существует в словаре. Кроме того, TryGetValue используется для получения данных, а не для изменения. Последующие вызовы каждого из этих методов могут привести к тому, что другой поток мог добавить или удалить ключ между двумя вызовами.
sgnsajgon
Это должен быть отмеченный ответ на вопрос, даже если сообщение Лассе отличное.
Spivonious
13

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

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

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

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

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

scope_creep
источник
4

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

ChaosPandion
источник
есть ли проблема, если я продолжу использовать Concurrent Dictionery?
kbvishnu
1

Это ConcurrentDictionaryотличный вариант, если он удовлетворяет все ваши потребности в безопасности потоков. Если это не так, другими словами, вы делаете что-то немного сложное, нормальный Dictionary+ lockможет быть лучшим вариантом. Например, предположим, что вы добавляете несколько заказов в словарь и хотите постоянно обновлять общую сумму заказов. Вы можете написать такой код:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Этот код не является потокобезопасным. Несколько потоков, обновляющих _totalAmountполе, могут оставить его в поврежденном состоянии. Так что вы можете попытаться защитить его с помощью lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Этот код «безопаснее», но все же не потокобезопасен. Нет никакой гарантии, что _totalAmountэто соответствует записям в словаре. Другой поток может попытаться прочитать эти значения, чтобы обновить элемент пользовательского интерфейса:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

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

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Этот код совершенно безопасен, но все преимущества использования a ConcurrentDictionary.

  1. Производительность стала хуже, чем при использовании обычного Dictionary, потому что внутренняя блокировка внутри ConcurrentDictionaryтеперь расточительна и избыточна.
  2. Вы должны проверить весь свой код на предмет незащищенного использования общих переменных.
  3. Вы застряли в использовании неудобного API ( TryAdd?, AddOrUpdate?).

Итак, мой совет: начните с Dictionary+ lockи оставьте вариант обновления позже до A ConcurrentDictionaryв качестве оптимизации производительности, если этот вариант действительно жизнеспособен. Во многих случаях это не так.

Теодор Зулиас
источник
0

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

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

Майкл Фрейдгейм
источник
ReadOnlyDictionary - это просто оболочка для другого IDictionary. Что вы используете под капотом?
Lu55 05
@ Lu55, мы использовали библиотеку MS .Net и выбирали среди доступных / применимых словарей. Я не знаю о внутренней реализации ReadOnlyDictionary в MS
Майкл Фрейдгейм