我有以下程序(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); } }); } } }
基本上我想缓存生产的光剑,但生产它们既昂贵又棘手 – 有时会出现例外情况.我想在给定的jedi时只允许一个生产者,但是当抛出异常时 – 我希望另一个生产者再试一次.因此,期望的行为类似于System.Lazy< T>.使用LazyThreadSafetyMode.ExecutionAndPublication选项,但没有例外缓存.
总而言之,必须满足以下技术要求:
>我们想要一个线程安全的缓存
>缓存是键值缓存.让我们简化它,键是字符串的类型,值也是字符串的类型
>生产一个项目是昂贵的 – 因此生产必须由一个且只有一个线程开始给定密钥.密钥“a”的生产不会阻止密钥“b”的生产
>如果生产成功结束 – 我们想要缓存生产的项目
>如果在抛出生产异常期间 – 我们希望将异常传递给调用者.呼叫者的责任是决定重试/放弃/记录.异常未缓存 – 下次调用此项目的缓存将启动项目生成.
在我的例子中:
>我们有LightsabersCache,LightsabersCache.GetLightsaber方法获取给定键的值
> LightsaberProvider只是虚拟提供者.它模仿生产性质:生产很昂贵(2秒),有时(在这种情况下只是第一次,对于key =“2”)异常被抛出
>程序启动15个线程,每个线程尝试10次以从范围< 0; 4>获取值.只抛出一次异常,所以只有一次我们应该看到“黑暗面发生了……”.在< 0; 4>范围内有5个键.所以控制台上只能有5个“Lightsaber产生的”消息.我们应该看到消息“LightsaberProvider.GetFor jedi:x”的6倍,因为每个密钥的一次因密钥“2”而失败.
解决方法
最好自己编写自己的Lazy实现
如果你打算只实例化引用类型,它会变得相当简单:
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()); } } }
附:也许我们将在this issue关闭时内置此功能.