Search code examples
c#.netmultithreadingcachinglazy-evaluation

Lazy<T> without exception caching


Is there a System.Lazy<T> without exception caching? Or another nice solution for lazy multithreading initialization & caching?

I've got following program (fiddle it here):

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using System.Net;

namespace ConsoleApplication3
{
    public class Program
    {
        public class LightsaberProvider
        {
            private static int _firstTime = 1;

            public LightsaberProvider()
            {
                Console.WriteLine("LightsaberProvider ctor");
            }

            public string GetFor(string jedi)
            {
                Console.WriteLine("LightsaberProvider.GetFor jedi: {0}", jedi);

                Thread.Sleep(TimeSpan.FromSeconds(1));
                if (jedi == "2" && 1 == Interlocked.Exchange(ref _firstTime, 0))
                {
                    throw new Exception("Dark side happened...");
                }

                Thread.Sleep(TimeSpan.FromSeconds(1));
                return string.Format("Lightsaver for: {0}", jedi);
            }
        }

        public class LightsabersCache
        {
            private readonly LightsaberProvider _lightsaberProvider;
            private readonly ConcurrentDictionary<string, Lazy<string>> _producedLightsabers;

            public LightsabersCache(LightsaberProvider lightsaberProvider)
            {
                _lightsaberProvider = lightsaberProvider;
                _producedLightsabers = new ConcurrentDictionary<string, Lazy<string>>();
            }

            public string GetLightsaber(string jedi)
            {
                Lazy<string> result;
                if (!_producedLightsabers.TryGetValue(jedi, out result))
                {
                    result = _producedLightsabers.GetOrAdd(jedi, key => new Lazy<string>(() =>
                    {
                        Console.WriteLine("Lazy Enter");
                        var light = _lightsaberProvider.GetFor(jedi);
                        Console.WriteLine("Lightsaber produced");
                        return light;
                    }, LazyThreadSafetyMode.ExecutionAndPublication));
                }
                return result.Value;
            }
        }

        public void Main()
        {
            Test();
            Console.WriteLine("Maximum 1 'Dark side happened...' strings on the console there should be. No more, no less.");
            Console.WriteLine("Maximum 5 lightsabers produced should be. No more, no less.");
        }

        private static void Test()
        {
            var cache = new LightsabersCache(new LightsaberProvider());

            Parallel.For(0, 15, t =>
            {
                for (int i = 0; i < 10; i++)
                {
                    try
                    {
                        var result = cache.GetLightsaber((t % 5).ToString());
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e.Message);
                    }
                    Thread.Sleep(25);
                }
            });
        }
    }
}

Basically I want to cache produced lightsabers, but producing them is expensive and tricky - sometimes exceptions may happen. I want to allow only one producer at time for given jedi, but when exception is thrown - I want another producer to try again. Therefore, desired behavior is like System.Lazy<T> with LazyThreadSafetyMode.ExecutionAndPublication option, but without exceptions caching.

All in all, following technical requirements must be meet:

  • we want a thread-safe cache
  • the cache is a key-value cache. Let's simplify it and the key is type of string and the value is also type of string
  • producing an item is expensive - thus production must be started by one and only one thread for given key. Production for key "a" doesn't block production for key "b"
  • if production ended in success - we want to cache the produced item
  • if during production exception is thrown - we want to pass the exception to the caller. The caller's responsibility is to decide about retry/giving up/logging. Exception isn't cached - next call to the cache for this item will start the item production.

In my example:

  • we have LightsabersCache, LightsabersCache.GetLightsaber method gets the value for given key
  • LightsaberProvider is only a dummy provider. It mimics production nature: the production is expensive (2 seconds), and sometimes (in this case only first time, for key="2") exception is thrown
  • the program starts 15 threads and each thread tries 10 times to get the value from range <0;4>. Only one time exception is thrown, so only one time we should see "Dark side happened...". There are 5 keys in the range <0;4> so only 5 "Lightsaber produced" messages should be on the console. We should see 6 times the message "LightsaberProvider.GetFor jedi: x" because one time for each key + one failed for key "2".

Solution

  • It's hard to use built-in Lazy for that: you should wrap your LazyWithoutExceptionCaching.Value getter in a lock. But that makes the use of the built-in Lazy redundant: you'll have unnecessary locks inside the Lazy.Value getter.

    It's better to write your own Lazy implementation especially if you intend to instantiate reference types only, it turns to be rather simple:

    public class SimpleLazy<T> where T : class
    {
        private readonly Func<T> valueFactory;
        private T instance;
        private readonly object locker = new object();
    
        public SimpleLazy(Func<T> valueFactory)
        {
            this.valueFactory = valueFactory;
            this.instance = null;
        }
    
        public T Value
        {
            get
            {
                lock (locker)
                    return instance ?? (instance = valueFactory());
            }
        }
    }
    

    P.S. Maybe we'll have this functionality built-in when this issue gets closed.