15

I have a scenario where I have to keep reference counted object for given key in the ConcurrentDictionary, if reference count reaches 0, I want to delete the key. This has to be thread safe hence I am planning to use the ConcurrentDictionary.

Sample program as follows. In the concurrent dictionary, I have key and value , the value is KeyValuePair which holds my custom object and reference count.

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));

Now I want a way to remove the key when the reference count reaches to 0. I was thinking , remove method on ConcurrentDictionary which takes key and predicate , removes the key if the predicate return 'true'. Example.

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

There is no such method on ConcurrentDictionary, question is how to do the same in thread safe way ?.

Let'sRefactor
  • 3,303
  • 4
  • 27
  • 43
bmadhu
  • 151
  • 6
  • 2
    Your `AddOrUpdate` is wrong. [While the methods are thread safe the delegates you pass in are not synchronized](http://stackoverflow.com/questions/10486579/concurrentdictionary-pitfall-are-delegates-factories-from-getoradd-and-addorup), this means two threads could do `pair.Value + 1` at the same time and one of the incrments would not be recorded. There also is no guarantee that the Update delegate will only be called once either, but you don't modify any external state in the update so you are safe there. – Scott Chamberlain Sep 24 '16 at 19:12
  • 1
    what is the way to synchronize the AddOrUpdate ?. – bmadhu Sep 24 '16 at 19:16
  • For your use case, I don't know. throwing a `lock` inside of it won't fix the problem because you need to block the call before it ever gets in to the update lambada. – Scott Chamberlain Sep 24 '16 at 19:18
  • 3
    @ScottChamberlain: I don't see the problem here. `KeyValuePair` is immutable. What's the problem if a `KeyValuePair` is created and then thrown away inside the `TryUpdate` loop if other thread updates the dictionary in the meantime? Also, I don't see what a `lock` would change, since the `pair` parameter would already be passed to the delegate. – vgru Sep 24 '16 at 19:19
  • If I see the documentation of AddOrUpdate overloaded function I am using , I do not see any remark regarding the thread safety . https://msdn.microsoft.com/en-us/library/ee378664(v=vs.110).aspx – bmadhu Sep 24 '16 at 19:20
  • @Groo Thread A calls it the first time and creates the object. Thread B and C call it at the same time. They both do `pair.Value + 1` where `pair.Value == 1`, this causes `2` to be saved twice. Putting the lock inside the lambada won't help because you already have your immutable object in the lambata and won't be told that the other instance was updated. – Scott Chamberlain Sep 24 '16 at 19:21
  • 2
    @Scott: but if thread `B` saves `2`, thread `C` will see that the initial value has changed, throw away its new value and retry. I presumed this is the way all concurrent collections work (i.e. loop until compare-exchange succeeds). – vgru Sep 24 '16 at 19:23
  • @Groo No, C already started before B finished. Also, bmadhum see [How to: Add and Remove Items from a ConcurrentDictionary](https://msdn.microsoft.com/en-us/library/dd997369.aspx), beneath the code example "*Also, although all methods of `ConcurrentDictionary` are thread-safe, not all methods are atomic, specifically GetOrAdd and AddOrUpdate. The user delegate that is passed to these methods is invoked outside of the dictionary's internal lock. (This is done to prevent unknown code from blocking all threads.) Therefore it is possible for this sequence of events to occur:*" – Scott Chamberlain Sep 24 '16 at 19:25
  • 2
    That MSDN remark doesn't state the dictionary will end up with two objects. It only states that the delegate might get called twice. But the key point is that thread A (from the example) will return the item that thread B created, which is a good thing. [Check the reference source](http://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentDictionary.cs,1093), you can see that `AddOrUpdate` calls `TryUpdate` after invoking the delegate, and then spins again if it fails. – vgru Sep 24 '16 at 19:34
  • I was looking at the similar functionality implemented in java in thread safe way. ConcurrentHashmap.merge. In this regard ConcurrentDictionary is very limited use . https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html#merge-K-V-java.util.function.BiFunction- – bmadhu Sep 24 '16 at 19:36
  • @Groo That example I linked to was showing a sequence of steps for `GetOrAdd`, GetOrUpdate has the same problem (it says so at the bottom) – Scott Chamberlain Sep 24 '16 at 19:40
  • 3
    @ScottChamberlain: yes, it does say *"it is not guaranteed that the data that is returned by `GetOrAdd` is the same data that was created by the thread's `valueFactory`"*, which only means `valueFactory` might be invoked several times. But I am specifically talking about `AddOrUpdate`. All these concurrent methods might invoke the factory delegate multiple times, but the end result will be transactionally consistent. – vgru Sep 24 '16 at 19:44
  • @Groo ,@ScottChamberlainI see the same remark for other overload of AddOrUpdate which takes 2 lambd's , but I do not see any remark for the overload I am using ( with 1 lambda) – bmadhu Sep 24 '16 at 19:45
  • 1
    @Groo I apologize, I came back to this question a year later and looking at the code I was totally wrong and you where right. He is not modifying external state in his update like I thought he was. – Scott Chamberlain Mar 28 '18 at 15:40
  • 1
    @ScottChamberlain: no hard feelings, it wouldn't be the first time I was wrong either, I am sometimes amazed when I look at some of my older answers :) – vgru Mar 28 '18 at 20:32

5 Answers5

20

.NET doesn't expose a RemoveIf directly, but it does expose the building blocks necessary to make it work without doing your own locking.

ConcurrentDictionary implements ICollection<T>, which has a Remove that takes and tests for a full KeyValuePair instead of just a key. Despite being hidden, this Remove is still thread-safe and we'll use it to implement this. One caveat for this to work is that Remove uses EqualityComparer<T>.Default to test the value, so it must be equality comparable. Your current one is not, so we'll re-implement that as such:

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?)?.Equals(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();
}

And finally, we'll define a method to increment/decrement counts from your dictionary:

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

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

The value for that key might be changed between the calls of AddOrUpdate and Remove, but that doesn't matter to us: because Remove tests the full KeyValuePair, it will only remove it if the value hasn't changed since the update.

This is the common lock-free pattern of setting up a change and then using a final thread-safe op to safely "commit" the change only if our data structure hasn't been updated in the meantime.

Cory Nelson
  • 29,236
  • 5
  • 72
  • 110
  • Instead of KeyValuePair should use ObjectCount class right ?. you need to correct sample, still using KeyValyePair for the initial value. – bmadhu Sep 26 '16 at 17:57
  • @JacobKrall there are no race conditions there, as explained in the answer. Are you thinking `KeyValuePair` or `ObjectCount` is a reference type, maybe? – Cory Nelson Sep 26 '16 at 22:30
  • Yep. Thanks for the correction and sorry for the antagonism. – Jacob Krall Sep 27 '16 at 17:51
  • This is a nice trick. But doesn't this solution suffer from the same problem that @scott-chamberlain mentions here: https://stackoverflow.com/questions/39679779/how-to-achieve-remove-if-functionality-in-net-concurrentdictionary#comment66660358_39679779 – Robert Hegner Jan 11 '18 at 16:24
  • 1
    @RobertHegner no, because the count is part of the atomic replace. so `(k, v+1)` will only be inserted if `(k, v)` is still the original `(k,v)` that you applied +1 to. Multiple threads can execute this, but only one will win. – Cory Nelson Jan 11 '18 at 16:28
  • According to http://blog.i3arnon.com/2018/01/16/concurrent-dictionary-tolist/ there is no guarantee that ICollection.Remove is thread-safe. Has anyone checked the code? – mattias Jan 19 '20 at 13:34
  • Addition: This person says he checked the code: https://blog.scooletz.com/2016/11/16/concurrent-conditional-deletes/ – mattias Jan 19 '20 at 13:39
  • It is not guaranteed to be thread safe by `ICollection`, but `ConcurrentDictionary`'s implementation is. Additionally, we recently exposed this API for .NET 5 as a public `ConcurrentDictionary.TryRemove(KeyValuePair keyValuePair)` method. – Cory Nelson Jan 19 '20 at 20:32
  • 1
    @CoryNelson How do I use TryRemove to check whether ConcurrentBag being used as Value is identical? I mainly need to check if ConcurrentBag is empty before removing it. Tried Comparing two instances of Empty ConcurrentBags but Equals fail, so can't use empty ConcurrentBag instance as value because it will always be false. – tunafish24 Jul 19 '22 at 12:03
7

You can't use a ConcurrentDictionary because it does not expose its internal locking. Your increment must occur under the same lock that controls the add (a simple interlocked add is not enough as a concurrent thread may remove the object before you increment the count). Similarly, the decrement must acquire the lock to be able to safely remove it if it reaches 0 count. What this spell is that you must use a dictionary for which you control the locking explicitly.

Remus Rusanu
  • 288,378
  • 40
  • 442
  • 569
  • Yeah i can't see why more isn't made of this! In the other examples after checking if the count is 0 its possible for the count to be incremented before the remove is called and that will cause all hell to break loose ... Its a real shame there isn't a remove if :( – Goz Apr 29 '21 at 16:20
1

I had a similar issue - having multi-threaded piece of code, I needed to count the number of times I've accessed certain type of resource. In other words - I need to find distribution of access to different resource types.

The way I've solved it:

Create store for your counts:

ConcurrentDictionary<string, StrongBox<int>> _counts = new ConcurrentDictionary<string, StrongBox<int>>();

When the resource is accessed, increment access count:

Interlocked.Increment(ref _counts.GetOrAdd(_resourceType, new StrongBox<int>(0)).Value);

In your case you'll have to take care of decrement as well.

I know it's not a full solution to the problem you've presented, and it's not a direct answer to it, but I hope it can be useful to someone.

Aryéh Radlé
  • 1,310
  • 14
  • 31
1

Currently (.NET 6) the ConcurrentDictionary<K,V> class has no API available that allows to update or remove a key, based on a user-supplied delegate. This functionality can be achieved laboriously by using the methods TryGetValue, TryUpdate and TryRemove in a loop:

string key = "mykey";
while (true)
{
    if (!ccd.TryGetValue(key, out KeyValuePair<object, int> existingValue))
        break; // The key was not found

    // Create new value based on the existing value
    KeyValuePair<object, int> newValue = KeyValuePair
        .Create(existingValue.Key, existingValue.Value - 1);

    if (newValue.Value > 0)
    {
        if (ccd.TryUpdate(key, newValue, existingValue))
            break; // The key was updated successfully
    }
    else
    {
        if (ccd.TryRemove(KeyValuePair.Create(key, existingValue)))
            break; // The key was removed successfully
    }

    // We lost the race to either TryUpdate or TryRemove. Try again.
}

In case there is no contention, the loop will exit after a single iteration.

I have made a proposal on GitHub, for an API TryUpdateOrRemove that would fill this void. In case this proposal is accepted, the above code could be reduced to this:

ccd.TryUpdateOrRemove(key, (_, existingValue) =>
{
    KeyValuePair<object, int> newValue = KeyValuePair
        .Create(existingValue.Key, existingValue.Value - 1);
    if (newValue.Value > 0) return (UpdateRemoveResult.Update, newValue);
    return (UpdateRemoveResult.Remove, default);
});

In case you like this proposal, make sure to give it an upvote on GitHub. Not only it's less code, but it should be also more efficient because the key would be hashed only once.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
0

This will give you a dictionary that tracks the count of an item if it is not zero and has no item when it is 0. The increment and decrement are fairly straightforward. The Remove empty node looks odd, but preserves accurate count even if adds and removes come in out of order. The decrement initial value of -1, again is to handle when calls come in out of order.

Concurrent programming is weird sometimes.

   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));
                }
            }
        }
    }
}
Jason Hernandez
  • 2,907
  • 1
  • 19
  • 30