
To be honest a have not spent too much time looking at .Net 4.0 beta 2 yet. But I do keep up with the blogs concerning Parallel Extensions. Josh Philps has just posted an entry about the changes that were made in the Coordination Data Structures (CDS) in Beta 2. The CDS are the types that have been added to the framework that will help writing concurrent applications without having to do (to much of) your own synchronization. I used two of the CDS classes (ConcurrentDictionary(Tkey, TVAlue) and Lazy<T>) in the CacheDictionary that I wrote about in this article.
1: public class CacheDictionary<TKey, TValue>
2: where TValue : class // needs to be a ref type due to current limitation of lazyInit<>
3: {
4: ConcurrentDictionary<TKey, LazyInit<TValue>> _cacheItemDictionary = new ConcurrentDictionary<TKey, LazyInit<TValue>>();
5:
6: public TValue Fetch(TKey key, Func<TValue> producer)
7: {
8: LazyInit<TValue> cacheItem;
9: if (!_cacheItemDictionary.TryGetValue(key, out cacheItem))
10: {
11: cacheItem = new LazyInit<TValue>(() => 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: }
In this cache I used ConcurrentDictionary<TKey, TValue> to store the cache items. To check if an item is present or should be created, I used TryAdd(key, cacheItem). This will return false if the item is allready present in the cache, in that case I assume the item can be retrieved from the dictionary with the indexer[]. This will work because the cache I implemented (unfortunately) does not yet support removals. If the cache would support any kind of mechanism to remove or invalidate items in the cache, the fetch method would have to handle the case of an item beeing removed between the TryAdd() and the retrieval.
The problem here is that up till beta 2 there was no good way to do the Add and “Get if allready present” in a single atomic operation. To work around this I would have to either use my own lock (that would remove all benefits of using a CuncurrentDictionary in the first place) or create a loop that keeps on calling TryAdd() and TryGetValue() until one of them succeeds.
I submitted this feedback to the parallel extensions forum with the suggestion to add an operation TryGetOrAddValue() that does these two operations in a single atomic method. I am pretty happy (and a bit proud 🙂 to see that this suggestion has found its way into the framework. The actual operation is now called GetorAdd(), (without the Try) which makes sense because either the Get or the Add will allways succeed. The PFX team really does seem to listen to custmer feedback!
2 comments
Hi Frank,
Thx for the idea of using Lazy with ConcurrentDictionary. does not provide a standard way to pass parameters to producer().
However this implementation CacheDictionary leaves an unresolved problem open: producer() needs to know the “key” to create its return value. Unfortunatelly Lazy
Using any inline implementation for producer(), and referencing the “key” local variable of Fetch method of course not a solution, because the return cacheItem.Value; statement potentiali can run in an other thread, with other actual “key” parameter than the thread which added the cacheItem to the dictionary.
horo
Hi Hero, thanks for your commens
What I did not describe in tis or any of my previous posts on this subject is the usage pattern for this style of cache. This might be a tipic for a next post.
In the way I use this cache, I actually do use an inline delegate for the producer that captures the key and potentially any other required variables from the surrounding scope to create the new value. This should not be a problem, because the cachekey is also used as the index in the underlying ConcurrentDictionary, so each different key will have a different instance of Lazy.
The new GetOrAdd operation on ConcurentDictionary does solve this problem by providing the key to the producer delegate. You could just use ConcurrentDictionary directly without the need for my CacheDictionary at all. The only thing you do net get from ConcurrentDictionary.GetOrAdd() is the guarantee thet the producer will be executed exectly once, depending on your scenario that might or might not be a problem.
frankb