HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Is this the correct usage of ConcurrentDictionary<TKey, TValue>?

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
thistkeytheconcurrentdictionaryusagecorrecttvalue

Problem

I need to make a simple class that tracks how many clients are listening to a specific item, will be referred to as building hence forth. I will have a timer running that will check which buildings are active and send updates to them. While this is occurring, the client will be able connect and disconnect. This means they will also be modifying the building list as time goes on.

To make sure that I don't have any concurrency issues, I decided to make a simple class that uses a ConcurrencyDictionary. I don't have a need of the class in the past and want to make sure it is being used correctly as the documentation seems to lack a bit for this class, mostly when it comes to updating values and the like.

```
///
/// Tracks the buildings that are actively being listened to and the number of clients subscribed to the building
///
public static class BuildingSubscriptionTracker
{
///
/// A dictionary of building name and listening client count
///
private static readonly ConcurrentDictionary _buildingsSubscribedTo = new ConcurrentDictionary();

///
/// Get all the buildings that are actively being listened to by clients
///
/// An of string
public static IEnumerable GetBuildings()
{
return _buildingsSubscribedTo.Keys;
}

///
/// Get how many clients are listening to updates for this building
///
/// Name of building to get client listening count for
/// A count of how many clients are listening to updates for this building. If the building is no longer being tracked, this will return 0
public static int GetActiveClientCountFor(string buildingName)
{
int clientCount;

if (_buildingsSubscribedTo.ContainsKey(buildingName))
if (_buildingsSubscribedTo.TryGetValue(buildingName, out clientCount))
return clientCount;

return 0;
}

///
/// Increment the client count of the given building
///
/// Name of the build

Solution

There isn't a lot of point of checking ContainsKey. A thread could sneak in between the ContainsKey and Try* methods you are calling. The TryGetValue out parameter will be initialized to the default value if it can't find the key.

The GetActiveClientCountFor could become this.

public static int GetActiveClientCountFor(string buildingName)
{
    int clientCount;
    _buildingsSubscribedTo.TryGetValue(buildingName, out clientCount);
    return clientCount;
}


In the IncrementClientCountFor code the value is retrieved and then added to. If someone came in and updated the value the TryUpdate would fail as the count changed.

What should be used is the AddOrUpdate method. If the key doesn't exist it will be created and set to one. If it does exist it will call the update method. The update method could be called multiple times if the value changed before it could be set and will be set to the last good value.

public static void IncrementClientCountFor(string buildingName)
{
    _buildingsSubscribedTo.AddOrUpdate(buildingName, 1, (s, i) => i++);
}


Same problem with DecrementClientCountFo. The value is retrieved and checked if less than one. But again a thread could add to dictionary and the code would remove it from the dictionary with a positive count. To get an Atomic TryRemove I would create an extension method called TryRemove (taken from "Little-known gems: Atomic conditional removals from ConcurrentDictionary"):

public static class ConcurrentDictionaryExtensions
{
    // cf. https://web.archive.org/web/20160603221138/http://blogs.msdn.com:80/b/pfxteam/archive/2011/04/02/10149222.aspx
    public static bool TryRemove(
        this ConcurrentDictionary dictionary, TKey key, TValue value)
    {
        if (dictionary == null) throw new ArgumentNullException("dictionary");
        return ((ICollection>) dictionary).Remove(
            new KeyValuePair(key, value));
    }
}


Now change DecrementClientCountFor to use AddOrUpdate then the Atomic TryRemove

public static void DecrementClientCountFor(string buildingName)
{
    var buildings = _buildingsSubscribedTo.AddOrUpdate(buildingName, s => 0, (s, i) => i--);
    if (buildings < 1)
    {
        _buildingsSubscribedTo.TryRemove(buildingName, buildings);
    }
}


If the value was changed from underneath us after getting the value the TryRemove will fail. Which is ok since it could be positive or another thread could have removed the key already.

Code Snippets

public static int GetActiveClientCountFor(string buildingName)
{
    int clientCount;
    _buildingsSubscribedTo.TryGetValue(buildingName, out clientCount);
    return clientCount;
}
public static void IncrementClientCountFor(string buildingName)
{
    _buildingsSubscribedTo.AddOrUpdate(buildingName, 1, (s, i) => i++);
}
public static class ConcurrentDictionaryExtensions
{
    // cf. https://web.archive.org/web/20160603221138/http://blogs.msdn.com:80/b/pfxteam/archive/2011/04/02/10149222.aspx
    public static bool TryRemove<TKey, TValue>(
        this ConcurrentDictionary<TKey, TValue> dictionary, TKey key, TValue value)
    {
        if (dictionary == null) throw new ArgumentNullException("dictionary");
        return ((ICollection<KeyValuePair<TKey, TValue>>) dictionary).Remove(
            new KeyValuePair<TKey, TValue>(key, value));
    }
}
public static void DecrementClientCountFor(string buildingName)
{
    var buildings = _buildingsSubscribedTo.AddOrUpdate(buildingName, s => 0, (s, i) => i--);
    if (buildings < 1)
    {
        _buildingsSubscribedTo.TryRemove(buildingName, buildings);
    }
}

Context

StackExchange Code Review Q#70453, answer score: 7

Revisions (0)

No revisions yet.