How to achieve remove_if functionality in .NET ConcurrentDictionary

.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.

Leave a Comment