День 1212. #ЗаметкиНаПолях #AsyncTips
Потокобезопасные словари
Задача
Имеется коллекция «ключ/значение» (например, кэш в памяти), которая должна поддерживаться в синхронизированном состоянии, даже если несколько потоков выполняют с ней операции чтения и записи.
Решение
Тип ConcurrentDictionary<TKey, TValue> является потокобезопасным гарантирует быстрый доступ в подавляющем большинстве сценариев. Его API сильно отличается от стандартного типа Dictionary<TKey, TValue>, поскольку он должен иметь дело с конкурентным доступом из многих потоков.
Запись
Чтобы задать значение для ключа, используйте метод AddOrUpdate:
var d = new ConcurrentDictionary<int, string>();
string newValue = d.AddOrUpdate(
0,
key => "Zero",
(key, oldValue) => "Zero"
);
Метод AddOrUpdate выглядит сложно, так как должен делать несколько вещей в зависимости от текущего содержимого словаря.
Аргументы:
- ключ,
- делегат, преобразующий ключ (в данном случае 0) в значение, которое будет добавлено в словарь (в данном случае "Zero"). Вызывается, только если ключа не существует в словаре.
- делегат, преобразующий ключ (0) и старое значение в обновлённое значение, которое должно быть сохранено в словаре ("Zero"). Вызывается, если ключ уже существует в словаре.
AddOrUpdate возвращает новое значение для этого ключа (значение, которое было возвращено одним из делегатов).
Чтобы конкурентный словарь работал правильно, может оказаться, что метод AddOrUpdate должен вызвать один (или оба) делегата несколько раз. Такое бывает очень редко, но это возможно. А значит, делегаты должны быть простыми и быстрыми и не должны иметь побочных эффектов, т.е. должны только создавать значение, не изменяя никакие другие переменные в приложении.
Добавить значение в словарь можно и через индекс:
// Добавляет (или обновляет) ключ 0,
// связывая с ним значение "Zero".
d[0] = "Zero";
Этот синтаксис не предоставляет возможности обновления значений на основании существующего значения. Но он проще и нормально работает, если известно значение, которое требуется сохранить в словаре.
Чтение
bool keyExists = d.TryGetValue(0, out string current);
TryGetValue вернёт true и задаст значение current, если ключ был найден в словаре. Если ключ не найден, TryGetValue вернет false. Прочитать значение можно и через индекс, но при отсутствии ключа будет выдано исключение.
Помните, что в конкурентном словаре несколько потоков могут заниматься чтением, обновлением, добавлением и удалением значений; во многих ситуациях бывает трудно проверить, существует ключ или нет до того, как вы попытаетесь прочитать его.
Удаление
Аналогично чтению:
bool keyExisted = d.TryRemove(0, out string removed);
Хотя ConcurrentDictionary<TKey, TValue> является потокобезопасным, это не означает атомарности его операций. Если несколько потоков вызывают AddOrUpdate конкурентно, может оказаться, что два потока обнаружат отсутствие ключа, а затем оба одновременно выполнят своего делегата, создающего новое значение.
ConcurrentDictionary<TKey, TValue> хорошо работает при чтении и записи со стороны нескольких потоков в общую коллекцию.
Если обновления относительно редки, возможно, ImmutableDictionary<TKey, TValue> будет более подходящим вариантом.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.