Вопрос

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

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

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

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

Это было полезно?

Решение

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

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

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

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

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

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

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

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

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

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

Другие советы

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

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

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

Внутренне 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-файл под названием Patterns of Paralell Programming (Шаблоны параллельного программирования).Его действительно стоит скачать, поскольку в нем действительно подробно описаны правильные шаблоны для использования в .Net 4 параллельных расширений и антишаблоны, которых следует избегать. Вот оно.

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

Мы использовали ConcurrentDictionary для кэшированной коллекции, которая повторно заполняется каждые 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);

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top