I am working on a caching manager for a MVC web application. For this app, I have some very large objects that are costly to build. During the application lifetime, I may need to create several of these objects, based upon user requests. When built, the user will be working with the data in the objects, resulting in many read actions. On occasion, I will need to update some minor data points in the cached object (create & replace would take too much time).
Below is a cache manager class that I have created to help me in this. Beyond basic thread safety, my goals were to:
- Allow multiple reads against a object, but lock all reads to that object upon an
update request - Ensure that the object is only ever created 1 time if
it does not already exist (keep in mind that its a long build
action). -
Allow the cache to store many objects, and maintain a lock
per object (rather than one lock for all objects).public class CacheManager { private static readonly ObjectCache Cache = MemoryCache.Default; private static readonly ConcurrentDictionary<string, ReaderWriterLockSlim> Locks = new ConcurrentDictionary<string, ReaderWriterLockSlim>(); private const int CacheLengthInHours = 1; public object AddOrGetExisting(string key, Func<object> factoryMethod) { Locks.GetOrAdd(key, new ReaderWriterLockSlim()); var policy = new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.Now.AddHours(CacheLengthInHours) }; return Cache.AddOrGetExisting (key, new Lazy<object>(factoryMethod), policy); } public object Get(string key) { var targetLock = AcquireLockObject(key); if (targetLock != null) { targetLock.EnterReadLock(); try { var cacheItem = Cache.GetCacheItem(key); if(cacheItem!= null) return cacheItem.Value; } finally { targetLock.ExitReadLock(); } } return null; } public void Update<T>(string key, Func<T, object> updateMethod) { var targetLock = AcquireLockObject(key); var targetItem = (Lazy<object>) Get(key); if (targetLock == null || key == null) return; targetLock.EnterWriteLock(); try { updateMethod((T)targetItem.Value); } finally { targetLock.ExitWriteLock(); } } private ReaderWriterLockSlim AcquireLockObject(string key) { return Locks.ContainsKey(key) ? Locks[key] : null; } }
Am I accomplishing my goals while remaining thread safe? Do you all see a better way to achieve my goals?
Thanks!
UPDATE: So the bottom line here was that I was really trying to do too much in 1 area. For some reason, I was convinced that managing the Get / Update operations in the same class that managed the cache was a good idea. After looking at Groo’s solution & rethinking the issue, I was able to do a good amount of refactoring which removed this issue I was facing.
Well, I don’t think this class does what you need.
You may lock all reads to the cache manager, but you are not locking reads (nor updates) to the actual cached instance.
I don’t think you ensured that. You are not locking anything while adding the object to the dictionary (and, furthermore, you are adding a lazy constructor, so you don’t even know when the object is going to be instantiated).Edit: This part holds, the only thing I would change is to make
Getreturn aLazy<object>. While writing my program, I forgot to cast it and callingToStringon the return value returned `”Value not created”.That’s the same as point 1: you are locking the dictionary, not the access to the object. And your
updatedelegate has a strange signature (it accepts a typed generic parameter, and returns anobjectwhich is never used). This means you are really modifying the object’s properties, and these changes are immediately visible to any part of your program holding a reference to that object.How to resolve this
If your object is mutable (and I presume it is), there is no way to ensure transactional consistency unless each of your properties also acquires a lock on each read access. A way to simplify this is to make it immutable (that why these are so popular for multithreading).
Alternatively, you may consider breaking this large object into smaller pieces and caching each piece separately, making them immutable if needed.
[Edit] Added a race condition example:
Something like this should happen: