Jamona Mican
Jamona Mican

Reputation: 1654

Best data structure for thread-safe list of subscriptions?

I am trying to build a subscription list. Let's take the example:

list of Publishers, each having a list of Magazines, each having a list of subscribers

Publishers --> Magazines --> Subscribers

Makes sense to use of a Dictionary within a Dictionary within a Dictionary in C#. Is it possible to do this without locking the entire structure when adding/removing a subscriber without race conditions?

Also the code gets messy very quickly in C# which makes me think I am not going down the right path. Is there an easier way to do this? Here are the constructor and subscribe method:

Note: The code uses Source, Type, Subscriber instead of the names above

Source ---> Type ---> Subscriber

public class SubscriptionCollection<SourceT, TypeT, SubscriberT>
{
// Race conditions here I'm sure! Not locking anything yet but should revisit at some point

ConcurrentDictionary<SourceT, ConcurrentDictionary<TypeT, ConcurrentDictionary<SubscriberT, SubscriptionInfo>>> SourceTypeSubs;

public SubscriptionCollection()
{
    SourceTypeSubs = new ConcurrentDictionary<SourceT, ConcurrentDictionary<TypeT, ConcurrentDictionary<SubscriberT, SubscriptionInfo>>>();
}

public void Subscribe(SourceT sourceT, TypeT typeT, SubscriberT subT) {

    ConcurrentDictionary<TypeT, ConcurrentDictionary<SubscriberT, SubscriptionInfo>> typesANDsubs;
    if (SourceTypeSubs.TryGetValue(sourceT, out typesANDsubs))
    {
        ConcurrentDictionary<SubscriberT, SubscriptionInfo> subs;
        if (typesANDsubs.TryGetValue(typeT, out subs))
        {

            SubscriptionInfo subInfo;
            if (subs.TryGetValue(subT, out subInfo))
            {
                // Subscription already exists - do nothing

            }
            else
            {
                subs.TryAdd(subT, new SubscriptionInfo());
            }
        }
        else
        {
            // This type does not exist - first add type, then subscription
            var newType = new ConcurrentDictionary<SubscriberT, SubscriptionInfo>();
            newType.TryAdd(subT, new SubscriptionInfo());
            typesANDsubs.TryAdd(typeT, newType);

        }

    }
    else
    {
        // this source does not exist - first add source, then type, then subscriptions
        var newSource = new ConcurrentDictionary<TypeT, ConcurrentDictionary<SubscriberT, SubscriptionInfo>>();
        var newType = new ConcurrentDictionary<SubscriberT, SubscriptionInfo>();
        newType.TryAdd(subT, new SubscriptionInfo());
        newSource.TryAdd(typeT, newType);
        SourceTypeSubs.TryAdd(sourceT, newSource);
    };
}

Upvotes: 1

Views: 1274

Answers (1)

svick
svick

Reputation: 245038

If you use ConcurrentDictionary, like you already do, you don't need locking, that's already taken care of.

But you still have to think about race conditions and how to deal with them. Fortunately, ConcurrentDictionary gives you exactly what you need. For example, if you have two threads, that both try to subscribe to source that doesn't exist yet at the same time, only one of them will succeed. But that's why TryAdd() returns whether the addition was successful. You can't just ignore its return value. If it returns false, you know some other thread already added that source, so you can retrieve the dictionary now.

Another option is to use the GetOrAdd() method. It retrieves already existing value, and creates it if it doesn't exist yet.

I would rewrite your code like this (and make it much simpler along the way):

public void Subscribe(SourceT sourceT, TypeT typeT, SubscriberT subT)
{
    var typesAndSubs = SourceTypeSubs.GetOrAdd(sourceT,
        _ => new ConcurrentDictionary<TypeT, ConcurrentDictionary<SubscriberT, SubscriptionInfo>>());

    var subs = typesAndSubs.GetOrAdd(typeT,
        _ => new ConcurrentDictionary<SubscriberT, SubscriptionInfo>());

    subs.GetOrAdd(subT, _ => new SubscriptionInfo());
}

Upvotes: 2

Related Questions