0

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?

shannon
  • 8,664
  • 5
  • 44
  • 74
  • What exact locking would be relying on? I haven't quite understood what you're trying to achieve yet... – Jon Skeet Dec 22 '13 at 10:04
  • Hey Jon. I'm not sure I understand your request for clarification; I'm pretty sure it's missing a word, but I can't figure out what one. I'll try to restate: I'm looking to replace an item in the dictionary without blocking the addition of other items, or blocking read or replace of other keyed values. Do I need the additional locking layer I've added with `AccountCacheLock` in my proprietary code above, or can I simply call `ConcurrentDictionary[key] =`? – shannon Dec 22 '13 at 10:35
  • Ultimately, I'm simply trying to create a cache for frequently-used Account (and child) information keyed on the Account Id. I was concerned that if I simply ask the concurrent dictionary to replace an item, I'll block all threads from using the cache for the duration of the replace. – shannon Dec 22 '13 at 10:41
  • I'll add another code sample above to clarify the question. – shannon Dec 22 '13 at 10:53

2 Answers2

2

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.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • I couldn't find `AccountClass` in my question. You may mean `AccountCache`. If so, it is intended to hold not just the Account object, but some related information, collapsed from a couple related objects and security rules. Also, it is intended to avoid caching the entire account object, when very few Account properties are referenced frequently. The caching of unnecessary properties (especially the full hierarchy) would not only increase the memory overhead, but more importantly increase the frequency that the item was invalidated in a simple cache implementation. Did I understand? – shannon Dec 22 '13 at 11:32
  • Yes, it's certainly possible that I'm entirely duplicating functionality present in the MemoryCache class. I don't dispute that. I'm going to investigate. – shannon Dec 22 '13 at 11:33
  • 1
    Sorry my bad. I meant `AccountCache`. If the entire Account object is too big to be cached consider using some view of it (a view model) that would be cached with a MemoryCache. IMHO caching is hard and I would recommend using the classes built into the framework instead of attempting to roll your own. – Darin Dimitrov Dec 22 '13 at 11:35
  • I hear you there, on avoiding complexity. Thank you. I'm not sure how I managed to erase MemoryCache from my own. I think I may instinctively avoid Object containers. – shannon Dec 22 '13 at 11:39
  • Also note that a cache-specific 'view model' is exactly what AccountCache is meant to be. It just happens to also have static methods to operate a cache. – shannon Dec 22 '13 at 13:11
  • I took a brief look inside ConcurrentDictionary and added a slightly more detailed answer to my title question. I left this marked as the answer, because it both broadly answered my title question, and also provided a solution to my actual issue. – shannon Dec 22 '13 at 14:17
0

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.

shannon
  • 8,664
  • 5
  • 44
  • 74