Developers who have started using the ConcurrentDictionary class in a multithreaded environment may have noticed some unusual behaviour with the GetOrAdd or AddOrUpdate methods exposed by the class. These methods are NOT performed in an atomic operation meaning the delegates parameters in these methods are not performed in a blocking manner. This can therefore cause the delegates to be called multiple times if multiple threads are accessing the dictionary at the same time. This may not have been your intention in the design of your code, especially if this delegate is processing a time consuming command (e.g. accessing a data layer).
For Microsoft to implement these methods this way is by design, this is because the ConcurrentDictionary class is a non-blocking data structure, whereby the delegates are run outside of the dictionary’s internal lock mechanism in order to prevent unknown code from blocking all threads accessing the dictionary. The design of this class is to keep access from multiple threads as quick as possible.
To get around this issue the ConcurrentDictionary can be used with the Lazy
class Program { static void Main(string[] args) { Consumer consumer = new Consumer(); Parallel.For(0, 10, (i) => consumer.Consume("NonLazyKey")); Parallel.For(0, 10, (i) => consumer.LazyConsume("LazyKey")); Console.ReadKey(); } } class Consumer { ConcurrentDictionary<string, int> _cache = new ConcurrentDictionary<string, int>(); ConcurrentDictionary<string, Lazy<int>> _lazyCache = new ConcurrentDictionary<string, Lazy<int>>(); public int Consume(string key) { return _cache.GetOrAdd(key, this.GetValue(key)); } public int LazyConsume(string key) { return _lazyCache.GetOrAdd(key, new Lazy<int>(() => this.GetValue(key))).Value; } int GetValue(string key) { Console.WriteLine("Getting Value for Key: {0}", key); return 1; } }
In the example provided the Consumer class implements two internal caches; one cache declared with a string key and an integer value, the other cache declared with a string key and a Lazy data type. Two methods are provided to get the integer values from the caches provided by a key, these both subsequently call the GetValue method to get a “calculate” value (this GetValue method could be long running). The GetValue method outputs to the Console every time it is called, this is done so that you can see how many times it is called by each Consume method. In this example the LazyComsume only ends up calling the GetValue method once, whereas the non-lazy method Consume calls the method multiple times.
The ConcurrentDictionary and Lazy classes should be used in conjunction with each other going forward as a safe design pattern to use to ensure your factory value generators aren't called multiple times.
No comments:
Post a Comment