Question

Does replacing a value associated with a ConcurrentDictionary key lock any dictionary operations beyond that key?

EDIT: For example, I'd like to know if either thread will ever block the other, besides when the keys are first added, in the following:

public static class Test {
    private static ConcurrentDictionary<int, int> cd = new ConcurrentDictionary<int, int>();
    public static Test() {
        new Thread(UpdateItem1).Start();
        new Thread(UpdateItem2).Start();
    }
    private static void UpdateItem1() {
        while (true) cd[1] = 0;
    }
    private static void UpdateItem2() {
        while (true) cd[2] = 0;
    }
}

Initially I assumed it does, because for example dictionary[key] = value; could refer to a key that is not present yet. However, while working I realized that if an add is necessary it could occur after a separate lock escalation.

I was drafting the following class, but the indirection provided by the AccountCacheLock class is unnecessary if the answer to this question (above) is "no". In fact, all of my own lock management is pretty much unneeded.

// A flattened subset of repository user values that are referenced for every member page access
public class AccountCache {

    // The AccountCacheLock wrapper allows the AccountCache item to be updated in a locally-confined account-specific lock.
    // Otherwise, one of the following would be necessary:
    // Replace a ConcurrentDictionary item, requiring a lock on the ConcurrentDictionary object (unless the ConcurrentDictionary internally implements similar indirection)
    // Update the contents of the AccountCache item, requiring either a copy to be returned or the lock to wrap the caller's use of it.
    private static readonly ConcurrentDictionary<int, AccountCacheLock> dictionary = new ConcurrentDictionary<int, AccountCacheLock>();

    public static AccountCache Get(int accountId, SiteEntities refreshSource) {
        AccountCacheLock accountCacheLock = dictionary.GetOrAdd(accountId, k => new AccountCacheLock());
        AccountCache accountCache;
        lock (accountCacheLock) {
            accountCache = accountCacheLock.AccountCache;
        }
        if (accountCache == null || accountCache.ExpiresOn < DateTime.UtcNow) {
            accountCache = new AccountCache(refreshSource.Accounts.Single(a => a.Id == accountId));
            lock (accountCacheLock) {
                accountCacheLock.AccountCache = accountCache;
            }
        }
        return accountCache;
    }

    public static void Invalidate(int accountId) {
        // TODO
    }

    private AccountCache(Account account) {
        ExpiresOn = DateTime.UtcNow.AddHours(1);
        Status = account.Status;
        CommunityRole = account.CommunityRole;
        Email = account.Email;
    }

    public readonly DateTime ExpiresOn;
    public readonly AccountStates Status;
    public readonly CommunityRoles CommunityRole;
    public readonly string Email;

    private class AccountCacheLock {
        public AccountCache AccountCache;
    }
}

Side question: is there something in the ASP.NET framework that already does this?

Was it helpful?

Solution

You don't need to be doing any locks. The ConcurrentDictionary should handle that pretty well.

Side question: is there something in the ASP.NET framework that already does this?

Of course. It's not specifically related to ASP.NET but you may take a look at the System.Runtime.Caching namespace and more specifically the MemoryCache class. It adds things like expiration and callbacks on the top of a thread safe hashtable.

I don't quite understand the purpose of the AccountCache you have shown in your updated answer. It's exactly what a simple caching layer gives you for free.

Obviously if you intend to be running your ASP.NET application in a web farm you should consider some distributed caching such as memcached for example. There are .NET implementations of the ObjectCache class on top of the memcached protocol.

OTHER TIPS

I also wanted to note that I took a cursory peek inside ConcurrentDictionary, and it looks like item replacements are locked on neither the individual item nor the entire dictionary, but rather the hash of the item (i.e. a lock object associated with a dictionary "bucket"). It seems to be designed so that an initial introduction of a key also does not lock the entire dictionary, provided the dictionary need not be resized. I believe this also means that two updates can occur simultaneously provided they don't produce matching hashes.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top