[Update: See this post for a cache that works with .Net 3.5 without PFX]
A implementation pattern I have seen quite often, uses a dictionary to store cached items based on a cache key and looks like the following:
1: Dictionary<int, Item> _cachedItems = new Dictionary<int, Item>();
2: object _lock = new object();
3:
4: public Item GetItem(int id)
5: {
6: Item result;
7: if(!_cacheItems.TryGetValue(id, out result))
8: {
9: lock(_lock)
10: {
11: if(!_cacheItems.TryGetValue(id, out result))
12: {
13: result = CreateItem(id); // does the actual expensive work of creating the item
14: _cacheItems.Add(id, result);
15: }
16: }
17: }
18: return result;
19: }
There are some problems with this implementation. The first and most important is that the MSDN documentation guarantees the dictionary only to be thread safe for multiple concurrent readers, not for reads and a write at the same time. In this implementation the first TryGetValue is done outside the lock and could conflict with an Add from an other thread. To get this right you will need to use more sophisticated locking using for instance a ReaderWriterLockSlim or always lock the entire cache even when only reading.
Another problem is that the CreateItem() Method, which does the actual expensive work to get the data we want to cache (e.g. call a Web Service), is done inside the lock that synchronizes access to the cache. This means that even if multiple different items are requested, only one item will be created at a time, this can be killing the performance you wanted to improve by caching. Finally this implementation requires the same pattern to be (wrongly) implemented over and over again because the code to create an item usually differs for every scenario.
To solve these problems once and for all I created a generic CacheDictionary using PFX. This CacheDictionary does not allow for items to be directly added or retrieved, instead the Fetch() method takes the key of the requested item and a delegate that will create the item if needed. If the item is found in the cache it will be returned, if not the provided delegate is invoked to create the item and store it for later use.
1: public class CacheDictionary
2: where TValue : class // needs to be a ref type due to current limitation of lazyInit<>
3: {
4: ConcurrentDictionary> _cacheItemDictionary = new ConcurrentDictionary >();
5:
6: public TValue Fetch(TKey key, Funcproducer)
7: {
8: LazyInitcacheItem;
9: if (!_cacheItemDictionary.TryGetValue(key, out cacheItem))
10: {
11: cacheItem = new LazyInit(() => producer(), LazyInitMode.EnsureSingleExecution);
12:
13: if (!_cacheItemDictionary.TryAdd(key, cacheItem))
14: {
15: // while we never remove items, if TryAdd fails it should be present
16: cacheItem = _cacheItemDictionary[key];
17: }
18: }
19: return cacheItem.Value;
20: }
21: }
To use the ConcurrentDictionary as a store for the cache the first step is to Try to Get the item with a TryGetValue(). If it is not found the new item is 'created' and I Try to Add it to the cache. This might fail if another thread beat me to it, in that case I can be sure the item exists (I don't support removes yet) and just get it using the indexer.
Inside the ConcurrentDictionary I don't store the cached items directly. Instead I wrap the actual items in another class introduced by the PFX, LazyInit
Note: In the current CTP of PFX, LazyInt
This CacheDictionary currently does not support expiration policies like for instance the ASP.Net web cache does. I found the ASP.Net cache however not to be very versatile while it only has one global cache store for an entire AppDomain and only allows string keys. Maybe pluggable expiration policies will be something for a next version of CacheDictionary, there are however a lot of scenario's where expiration is not really an issue and it is of course always possible to flush the entire CacheDictionary by just creating a new instance :-). Any suggestions for improvement however are welcome.
No comments:
Post a Comment