День двести пятьдесят восьмой. #ЗаметкиНаПолях

Использование потокобезопасных коллекций. Продолжение

ConcurrentDictionary

Словарь предоставляет хранилище данных, проиндексированное по ключу. ConcurrentDictionary<TKey, TValue> может использоваться несколькими параллельными задачами. Действия над словарем выполняются атомарно. Другими словами, действие по обновлению элемента в словаре не может быть прервано действием из другой задачи. ConcurrentDictionary предоставляет некоторые дополнительные методы, которые необходимы, когда словарь используется несколькими задачами.

ConcurrentDictionary<string, int> ages = new ConcurrentDictionary<string, int>();

if (ages.TryAdd("Иван", 21))

Console.WriteLine("Иван успешно добавлен.");

Console.WriteLine("Возраст Ивана: {0}", ages["Иван"]);

// Пытаемся изменить возраст с 21 на 22

if (ages.TryUpdate("Иван", 22, 21))

Console.WriteLine("Возраст успешно обновлён");

Console.WriteLine("Новый возраст Ивана: {0}", ages["Иван"]);

// Увеличиваем возраст, используя фабричный метод

Console.WriteLine("Возраст Ивана обновлён до: {0}",

ages.AddOrUpdate("Иван", 1,

(name,age) => age = age+1));

Console.WriteLine("Новый возраст Ивана: {0}", ages["Иван"]);

Метод TryAdd пытается добавить новый элемент. Если элемент уже существует, метод TryAdd возвращает false. Метод TryUpdate поставляется с ключом обновляемого элемента, новым значением, которое должно быть сохранено в элементе, и значением, которое должно быть перезаписано. В приведенном выше примере возраст элемента «Иван», будет обновлен до 22, только если существующее значение равно 21. Это позволяет программе обновлять элемент, только если он имеет ожидаемое значение.

Метод AddOrUpdate позволяет предоставить поведение, которое будет выполнять обновление заданного элемента или добавлять новый элемент, если он ещё не существует. В приведённом выше примере добавление элемента «Иван» не будет выполнено при вызове AddOrUpdate, поскольку элемент уже существует. Вместо этого выполняется пользовательский делегат, передаваемый в качестве третьего аргумента, который увеличивает возраст элемента «Иван» на 1.

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

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

Кроме того, хотя все методы ConcurrentDictionary являются потокобезопасными, не все методы являются атомарными, в частности GetOrAdd и AddOrUpdate. Пользовательский делегат, который передается этим методам, вызывается вне внутренней блокировки словаря (это делается для того, чтобы неизвестный код не блокировал все потоки). Следовательно, возможна следующая последовательность событий:

1) Поток A вызывает GetOrAdd, не находит элемента и создаёт новый элемент для добавления, вызывая делегат.

2) Поток B одновременно вызывает GetOrAdd, вызывается его делегат, и он достигает внутренней блокировки словаря перед потоком A, поэтому его новая пара ключ-значение добавляется в словарь.

3) Продолжается выполнение делегата из потока A, он достигает блокировки, но теперь видит, что элемент уже существует.

4) Поток A выполняет «Get» и возвращает данные, которые были ранее добавлены потоком B.

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



Источники:

- Rob Miles “Exam Ref 70-483 Programming in C#”. 2nd ed - Pearson Education, Inc., 2019. Глава 1.

-
https://docs.microsoft.com/dotnet/standard/collections/thread-safe/how-to-add-and-remove-items