.NET - блокировка словаря и ConcurrentDictionary
Я не мог найти достаточно информации о ConcurrentDictionary
типы, поэтому я подумал, что я хотел бы спросить об этом здесь.
В настоящее время я использую Dictionary
для хранения всех пользователей, к которым постоянно обращаются несколько потоков (из пула потоков, поэтому нет точного количества потоков), и он имеет синхронизированный доступ.
Недавно я обнаружил, что в.NET 4.0 был набор потоковобезопасных коллекций, и это кажется очень приятным. Мне было интересно, что будет "более эффективным и простым в управлении" вариантом, так как у меня есть выбор между нормальным Dictionary
с синхронизированным доступом, или иметь ConcurrentDictionary
который уже потокобезопасен.
Ссылка на.NET 4.0ConcurrentDictionary
7 ответов
Потоково-безопасная коллекция и не-безопасная коллекция могут рассматриваться по-другому.
Рассмотрим магазин без клерка, кроме как на кассе. У вас масса проблем, если люди не действуют ответственно. Например, скажем, покупатель берет банку из банки с пирамидой, в то время как клерк в настоящее время строит пирамиду, и весь ад развалится. Или, что, если два клиента дотягиваются до одного и того же товара одновременно, кто победит? Будет ли бой? Это не потокобезопасная коллекция. Существует множество способов избежать проблем, но все они требуют какой-то блокировки или, скорее, явного доступа, так или иначе.
С другой стороны, рассмотрите магазин с клерком за столом, и вы можете делать покупки только через него. Вы встаете в очередь и просите у него предмет, он возвращает его вам, и вы выходите из очереди. Если вам нужно несколько предметов, вы можете забрать в каждую поездку столько предметов, сколько вы помните, но вы должны быть осторожны, чтобы не приставать клерку, это разозлит других клиентов в очереди за вами.
Теперь рассмотрим это. В магазине с одним клерком, что если вы доберетесь до самого конца очереди и спросите у клерка "У вас есть туалетная бумага", он скажет "Да", а затем вы скажете: "Хорошо, я" Я перезвоню вам, когда узнаю, сколько мне нужно ", и к тому времени, когда вы вернетесь к началу очереди, магазин, конечно, может быть распродан. Этот сценарий не предотвращается потокобезопасной коллекцией.
Потокобезопасная коллекция гарантирует, что ее внутренние структуры данных всегда действительны, даже если доступ осуществляется из нескольких потоков.
Коллекция без потоков не имеет таких гарантий. Например, если вы добавляете что-то в двоичное дерево в одном потоке, в то время как другой поток занят перебалансировкой дерева, нет никакой гарантии, что элемент будет добавлен, или даже то, что дерево все еще остается действительным после этого, это может быть повреждено без надежды.
Тем не менее, потокобезопасная коллекция не гарантирует, что все последовательные операции над потоком работают с одним и тем же "снимком" его внутренней структуры данных, что означает, что если у вас есть такой код:
if (tree.Count > 0)
Debug.WriteLine(tree.First().ToString());
вы можете получить NullReferenceException, потому что между tree.Count
а также tree.First()
другой поток очистил оставшиеся узлы в дереве, что означает First()
вернусь null
,
Для этого сценария вам либо нужно проверить, есть ли у рассматриваемой коллекции безопасный способ получить то, что вы хотите, возможно, вам нужно переписать приведенный выше код или вам может потребоваться блокировка.
Вы все еще должны быть очень осторожны при использовании многопоточных коллекций, потому что поточно-безопасных не означает, что вы можете игнорировать все проблемы с потоками. Когда коллекция объявляет себя поточно-ориентированной, это обычно означает, что она остается в согласованном состоянии, даже когда несколько потоков читают и пишут одновременно. Но это не означает, что один поток увидит "логическую" последовательность результатов, если вызовет несколько методов.
Например, если вы сначала проверите, существует ли ключ, а затем получите значение, соответствующее ключу, этот ключ может больше не существовать даже с версией ConcurrentDictionary (поскольку другой поток мог удалить ключ). В этом случае вам все еще нужно использовать блокировку (или лучше: объединить два вызова с помощью TryGetValue).
Так что используйте их, но не думайте, что это дает вам свободный проход, чтобы игнорировать все проблемы параллелизма. Вы все еще должны быть осторожны.
Внутренне ConcurrentDictionary использует отдельную блокировку для каждого сегмента хэша. Пока вы используете только Add/TryGetValue и подобные методы, которые работают с отдельными записями, словарь будет работать как практически свободная от блокировки структура данных с соответствующим приятным выигрышем в производительности. OTOH методы перечисления (включая свойство Count) блокируют все сегменты сразу и поэтому хуже, чем синхронизированный словарь, с точки зрения производительности.
Я бы сказал, просто используйте ConcurrentDictionary.
Я думаю, что метод ConcurrentDictionary.GetOrAdd - это именно то, что нужно большинству многопоточных сценариев.
Вы видели Реактивные расширения для.Net 3.5sp1. По словам Джона Скита, они поддержали пакет параллельных расширений и параллельных структур данных для.Net3.5 sp1.
Существует набор примеров для.Net 4 Beta 2, который довольно подробно описывает, как использовать их для параллельных расширений.
Я только что провел последнюю неделю, тестируя ConcurrentDictionary, используя 32 потока для выполнения операций ввода-вывода. Кажется, он работает так, как рекламируется, что указывает на огромное количество испытаний.
Изменить: .NET 4 ConcurrentDictionary и шаблоны.
Microsoft выпустила PDF-файл под названием "Шаблоны программирования Paralell". Его действительно стоит скачать, так как он очень подробно описывает правильные шаблоны, которые следует использовать для расширений.Net 4 Concurrent, и анти-шаблоны, которых следует избегать. Вот.
По сути, вы хотите использовать новый ConcurrentDictionary. Прямо из коробки вы должны написать меньше кода, чтобы сделать потокобезопасные программы.
Мы использовали ConcurrentDictionary для кэшированной коллекции, которая обновляется каждые 1 час, а затем читается несколькими клиентскими потоками, аналогично решению для Безопасного потока в этом примере? вопрос.
Мы обнаружили, что изменение его на ReadOnlyDictionary улучшило общую производительность.
В 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);
Этот код совершенно безопасен, но все преимущества использования ConcurrentDictionary
ушли.
- Производительность стала хуже, чем при использовании обычного
Dictionary
, потому что внутренняя блокировка внутриConcurrentDictionary
теперь расточительно и излишне. - Вы должны проверить весь свой код на предмет незащищенного использования общих переменных.
- Вы застряли в использовании неудобного API (
TryAdd
?,AddOrUpdate
?).
Итак, мой совет: начните с Dictionary
+lock
, и оставьте возможность обновиться позже до ConcurrentDictionary
в качестве оптимизации производительности, если этот вариант действительно жизнеспособен. Во многих случаях это не так.
Есть хороший курс о параллельных коллекциях в C# под капотом: параллельные коллекции Pluralsight
Вы можете найти всю интересную информацию там.