11

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".
piotrwest
  • 2,022
  • 21
  • 32

7 Answers7

6

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.

tsul
  • 1,260
  • 17
  • 19
6

Actualy, this feature is debated : https://github.com/dotnet/corefx/issues/32337

To wait, I use this graceful implementation from Marius Gundersen : https://github.com/alastairtree/LazyCache/issues/73

public class AtomicLazy<T>
{
    private readonly Func<T> _factory;
    private T _value;
    private bool _initialized;
    private object _lock;

    public AtomicLazy(Func<T> factory)
    {
        _factory = factory;
    }

    public T Value => LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref _lock, _factory);
}
vernou
  • 4,372
  • 2
  • 23
  • 48
5

Unfortunately this is wrong solution! Please disregard it and use tsul answer. Leaving it only if you want to debug it and spot the bug.

Here is working solution (concurrent cache with factory) with tsul SimpleLazy: https://dotnetfiddle.net/Y2GP2z


I've ended up with following solution: wrapped Lazy to mimic the same functionality as Lazy but without exceptions cache.

Here is LazyWithoutExceptionsCaching class:

public class LazyWithoutExceptionCaching<T>
{
    private readonly Func<T> _valueFactory;
    private Lazy<T> _lazy;
     
    public LazyWithoutExceptionCaching(Func<T> valueFactory)
    {
        _valueFactory = valueFactory;
        _lazy = new Lazy<T>(valueFactory);
    }

    public T Value
    {
        get
        {
            try
            {
                return _lazy.Value;
            }
            catch (Exception)
            {
                _lazy = new Lazy<T>(_valueFactory);
                throw;
            }
        }
    }
}

Full working example (FIDDLE it here):

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

namespace Rextester
{
    public class Program
    {
        public class LazyWithoutExceptionCaching<T>
        {
            private readonly Func<T> _valueFactory;
            private Lazy<T> _lazy;
             
            public LazyWithoutExceptionCaching(Func<T> valueFactory)
            {
                _valueFactory = valueFactory;
                _lazy = new Lazy<T>(valueFactory);
            }
    
            public T Value
            {
                get
                {
                    try
                    {
                        return _lazy.Value;
                    }
                    catch (Exception)
                    {
                        _lazy = new Lazy<T>(_valueFactory);
                        throw;
                    }
                }
            }
        }
        
        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, LazyWithoutExceptionCaching<string>> _producedLightsabers;

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

            public string GetLightsaber(string jedi)
            {
                LazyWithoutExceptionCaching<string> result;
                if (!_producedLightsabers.TryGetValue(jedi, out result))
                {
                    result = _producedLightsabers.GetOrAdd(jedi, key => new LazyWithoutExceptionCaching<string>(() =>
                    {
                        Console.WriteLine("Lazy Enter");
                        var light = _lightsaberProvider.GetFor(jedi);
                        Console.WriteLine("Lightsaber produced");
                        return light;
                    }));
                }
                return result.Value;
            }
        }
        
        public static void Main(string[] args)
        {
            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);
                }
            });
        }
    }
}
Community
  • 1
  • 1
piotrwest
  • 2,022
  • 21
  • 32
  • 4
    unfortunately this is not thread safe, since two threads can write to _lazy at the same time. You probably need a lock around it too. – Marius Jun 23 '16 at 09:46
  • Well, isn't that the purpose of Lazy? IMO you don't need a lock. – piotrwest Jun 26 '16 at 11:55
  • 3
    yes, but the lock should be around the try/catch, not inside it. Two threads can get two different values if they try at the same time, so it's not thread safe. – Marius Jun 27 '16 at 08:52
  • 1
    Unfortunately, it's really not thread safe: https://dotnetfiddle.net/Q4oYi8 It leads to valueFactory called more than once even when succeeded, that subverts all the idea of Lazy. – tsul Oct 29 '18 at 20:59
  • 1
    I see, you are completely right guys. I've changed accepted answer, and edited this answer stating the problem. – piotrwest Nov 02 '18 at 03:55
2

As I mentioned in comment, you can ease your code by using the TPL library's Task object:

var resultTask = Task.Factory.StartNew(new Action<object>(
  (x) => GetFor(x)), rawData);
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);
}

After that, you can wait for the result of this task like this:

resultTask.Wait();

Making this will cache the result of the operation for concrete x. If task runs correctly, you can examine the Result property. If task fails, the Exception property will store the AggregateException with inner actual exception. Result is cached and will not re-calculated. If task fails, it will throw its exception at calling the Result property or some other blocking methods of it. If you need a result for different argument, you should create a new task.

I encourage you to examine this library as you'll save your time for re-inventing the wheel :) Also you'll got some out-of box functionality as multithreading, exception handling, task cancellation and many many more. Good luck with your projects :)

VMAtm
  • 27,342
  • 17
  • 79
  • 118
  • I understand the concept of replacing Lazy with Task. However, how would you start exactly one task? ConcurrentDictionary can create redundant values when invoking method GetOrAdd with factory method. One more thing - even if you manage to block invocation of factory method multiple times, how would you replace a task which throws exception in result? All in all, idea is of caching result is great, but I think it won't work in this situation with cache and concurrent dictionary. Feel free to look at expected result in my answer with LazyWithoutExceptionsCaching. – piotrwest Dec 22 '15 at 22:29
  • I'll update my answer but want to mention that you should not use `TryGetValue` - you can simply directly call `GetOrAdd` - it works right the same way. – VMAtm Dec 23 '15 at 09:32
  • @piotrwest Oh, I got it! You store the exception, and if it throws, you replace the value? Ok, and what if it throws second time? – VMAtm Dec 23 '15 at 09:41
  • @piotrwest No, still didn't understand your concerns regarding the `Tasks`, I think it will fit. – VMAtm Dec 23 '15 at 09:43
  • Ok, I'll update the question to explain a little bit more how it should work. It has pretty reasonable technical requirements and it would be good to know if it can be solved better. – piotrwest Dec 23 '15 at 09:54
  • @piotrwest I see your additions, and yes, my approach will not suit for them - you'll have to create a dictionary for the tasks, not the results, which can be very complicated – VMAtm Dec 23 '15 at 10:48
  • Sorry for not specifying them beforehand. Anyway, if you can think of any better solution than mine (which seems to be working fine) - let me know. – piotrwest Dec 23 '15 at 11:31
0

Created this class based on @piotrwest as an improvement !

internal class CustomLazy<T> where T : class
{
    private readonly Func<T> _valueFactory;
    private Lazy<T> _lazy;
    private int _counter;

    public T Value => _lazy.Value;

    public CustomLazy( Func<T> valueFactory )
    {
        _valueFactory = valueFactory;
        _counter = 0;            

        _lazy = new Lazy<T>( 
            Create,
            LazyThreadSafetyMode.PublicationOnly 
        );
    }

    private T Create()
    {
        try
        {
            if( Interlocked.Increment( ref _counter ) == 1 )
            {
                return _valueFactory();
            }
            else
            {
                throw new InvalidOperationException( );
            }
        }
        finally
        {
            Interlocked.Decrement( ref _counter );
        }
    }
}

Configuring Lazy instance with LazyThreadSafetyMode.PublicationOnly makes it possible to retry until you get your desired value but it also allow for multiple Create functions to be called at the same time. To counter that mechanic I've added a ref counter to allow only one valueFactory to called be at the same time. You should consider using this only where you can manage failure from Value property.

Dharman
  • 26,923
  • 21
  • 73
  • 125
-1

Better way:

public class SimpleLazy<T> where T : class
{
    private readonly Func<T> valueFactory;
    private T instance;

    public SimpleLazy(Func<T> valueFactory)
    {
        this.valueFactory = valueFactory;
        this.instance = null;
    }

    public T Value
    {
        get
        {
            return LazyInitializer.EnsureInitialized(ref instance, valueFactory);
        }
    }
}
volt
  • 9
  • 1
  • I don't think so. Although `LazyInitializer.EnsureInitialized` will only assign `instance` once, it potentially executes `valueFactory()` many times - which rather defeats the original purpose. **"In the event that multiple threads access this method concurrently, multiple instances of T may be created, but only one will be stored into target and returned"** https://docs.microsoft.com/en-us/dotnet/api/system.threading.lazyinitializer.ensureinitialized?view=net-5.0 – Phil B Mar 10 '21 at 13:33
-1

Have a look at this page: https://docs.microsoft.com/en-us/dotnet/api/system.lazy-1.isvaluecreated?view=netcore-3.1

The key point is that if an error occurs within the lazy function, you get IsValueCreated = false

You can initialize the lazy object with the LazyThreadSafetyMode option. With PublicationOnly option, the Lazy class caches the value only if IsValueCreated = true.

Please do not reinvent the wheel.

  • If you switch the thread-safety mode to `PublicationOnly` then it changes the initialization behavior to allow many threads to run the initializer simultaneously, which is a key difference between what you've suggested, and what the OP asked for: **"I want to allow only one producer at time for given jedi, but when exception is thrown - I want another producer to try again".** – Phil B Mar 10 '21 at 13:46