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".
Task
with continuation on error handler – Tanbark