4

У меня есть сценарий, в котором мне нужно сохранить подсчитанный объект для заданного ключа в ConcurrentDictionary, если количество ссылок достигает 0, я хочу удалить ключ. Это должно быть потокобезопасным, поэтому я планирую использовать ConcurrentDictionary.Как достичь функции remove_if в .NET ConcurrentDictionary

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

ConcurrentDictionary<string, KeyValuePair<object, int>> ccd = 
    new ConcurrentDictionary<string, KeyValuePair<object, int>>(); 

// following code adds the key, if not exists with reference 
// count for my custom object to 1 
// if the key already exists it increments the reference count 

var addOrUpdateValue = ccd.AddOrUpdate("mykey", 
    new KeyValuePair<object, int>(new object(), 1), 
    (k, pair) => new KeyValuePair<object, int>(pair.Key, pair.Value + 1)); 

Теперь я хочу способ, чтобы удалить ключ, когда счетчик ссылок достигает 0. Я думал, удалить метод на ConcurrentDictionary который принимает ключ и предикат, удаляет ключ, если возвращение предикат «истинный». Пример.

ConcurrentDictionary.remove(TKey, Predicate<TValue>). 

Там нет такого метода на ConcurrentDictionary, вопрос заключается в том, чтобы сделать то же самое в потоке безопасно?.

+1

Ваш «AddOrUpdate» ошибочен.[Хотя методы являются потокобезопасными, делегаты, которых вы передаете, не синхронизированы] (http://stackoverflow.com/questions/10486579/concurrentdictionary-pitfall-are-delegates-factories-from-getoradd-and-addorup), это означает два потока могли бы сделать пару. Value + 1' одновременно, и одно из них не было записано. Также не гарантируется, что делегат Update будет вызываться только один раз, но вы не изменяете какое-либо внешнее состояние в обновлении, чтобы вы были в безопасности там. –

+0

Каков способ синхронизации AddOrUpdate?. – bmadhu

+0

Для вашего случая использования, я не знаю. бросание «блокировки» внутри него не решит проблему, потому что вам нужно заблокировать вызов, прежде чем он когда-либо попадет в обновление lambada. –

ответ

3

Вы не можете использовать ConcurrentDictionary, потому что он не предоставляет свою внутреннюю блокировку. Ваш прирост должен происходить под той же блокировкой, которая управляет добавлением (простого взаимоблокированного добавления недостаточно, поскольку параллельный поток может удалить объект, прежде чем увеличивать счет). Аналогично, декремент должен получить блокировку, чтобы иметь возможность безопасно удалить ее, если она достигает 0 отсчетов. Это заклинание заключается в том, что вы должны использовать словарь, для которого вы явно контролируете блокировку.

2

. NET не раскрывает RemoveIf непосредственно, но он раскрывает строительные блоки, необходимые, чтобы заставить его работать, не делая свою собственную блокировку.

ConcurrentDictionary реализует ICollection<T>, который имеет Remove, который принимает и тесты для полного KeyValuePair, а не только ключ. Несмотря на то, что он скрыт, этот Remove по-прежнему является потокобезопасным, и мы будем использовать его для его реализации. Одно из предостережений для этого заключается в том, что Remove использует EqualityComparer<T>.Default для проверки значения, поэтому оно должно быть равноценным. Ваш текущий нет, поэтому мы будем повторно реализовать как таковые:

struct ObjectCount : IEquatable<ObjectCount> 
{ 
    public object Object { get; } 
    public int Count { get; } 

    public ObjectCount(object o, int c) 
    { 
     Object = o; 
     Count = c; 
    } 

    public bool Equals(ObjectCount o) => 
     object.Equals(Object, o.Object) && Count == o.Count; 

    public override bool Equals(object o) => 
     (o as ObjectCount?)?.Equal(this) == true; 

    // this hash combining will work but you can do better. 
    // it is not actually used by any of this code. 
    public override int GetHashCode() => 
     (Object?.GetHashCode() ?? 0)^Count.GetHashCode(); 
} 

И, наконец, мы определим метод для увеличения отсчетов/декремента из словаря:

void UpdateCounts(ConcurrentDictionary<string, ObjectCount> dict, string key, int toAdd) 
{ 
    var addOrUpdateValue = ccd.AddOrUpdate(key, 
     new ObjectCount(new object(), 1), 
     (k, pair) => new ObjectCount(pair.Key, pair.Value + toAdd)); 

    if(addOrUpdateValue.Count == 0) 
    { 
     (ICollection<KeyValuePair<string, ObjectCount>>).Remove(addOrUpdateValue); 
    } 
} 

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

Это обычный шаблон без блокировки, позволяющий настроить изменение, а затем использовать окончательный потокобезопасный режим, чтобы безопасно «зафиксировать» изменение только в том случае, если наша структура данных не была обновлена ​​в то же время.

+0

Вместо KeyValuePair следует использовать класс ObjectCount правильно. вам нужно исправить выборку, все еще используя KeyValyePair для начального значения. – bmadhu

+0

woops, спасибо. исправлено. –

+0

@JacobKrall там нет условий гонки, как объясняется в ответе. Вы думаете, что «KeyValuePair» или «ObjectCount» является ссылочным типом, может быть? –

0

Это даст вам словарь, который отслеживает количество элементов, если он не равен нулю и не имеет элемента, когда он равен 0. Инкремент и декремент довольно просты. Удаленный пустой узел выглядит нечетным, но сохраняет точный счет, даже если добавление и удаление выходят из строя. Начальное значение декремента -1, снова должно обрабатываться, когда вызовы выходят из строя.

Параллельное программирование иногда бывает странным.

private void Increment(string key) 
    { 
     var result = ccd.AddOrUpdate(key,new KeyValuePair<object, int>(new object(), 1),(k, pair) => new KeyValuePair<object, int>(pair.Key, pair.Value + 1)); 
     RemoveEmptyNode(key, result); 

    } 

    private void Decrement(string key) 
    { 
     var result = ccd.AddOrUpdate(key, new KeyValuePair<object, int>(new object(), -1), (k, pair) => new KeyValuePair<object, int>(pair.Key, pair.Value - 1)); 
     RemoveEmptyNode(key, result); 
    } 

    private void RemoveEmptyNode(string key, KeyValuePair<object, int> result) 
    { 
     if (result.Value == 0) 
     { 
      KeyValuePair<object, int> removedKeyValuePair; 
      if (ccd.TryRemove(key, out removedKeyValuePair)) 
      { 
       if (removedKeyValuePair.Value != 0) 
       { 
        ccd.AddOrUpdate(key, removedKeyValuePair, 
         (k, pair) => new KeyValuePair<object, int>(key, pair.Value + removedKeyValuePair.Value)); 
       } 
      } 
     } 
    } 
}