pio*_*est 9 .net c# multithreading caching lazy-evaluation
是否System.Lazy<T>有无例外的缓存?或者懒惰的多线程初始化和缓存的另一个很好的解决方案?
我有以下程序(在这里小提琴):
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);
}
});
}
}
}
Run Code Online (Sandbox Code Playgroud)
基本上我想缓存生产的光剑,但生产它们既昂贵又棘手 - 有时会出现例外情况.我想在给定时只允许一个生产者jedi,但是当抛出异常时 - 我希望另一个生产者再试一次.因此,期望的行为System.Lazy<T>与LazyThreadSafetyMode.ExecutionAndPublication选项类似,但没有例外缓存.
总而言之,必须满足以下技术要求:
在我的例子中:
vernou和tsul(AtomicLazy<T>分别)的两个现有答案SimpleLazy<T>充分解决了这个问题,但它们都表现出一种并不完全符合我喜欢的行为。如果valueFactory失败,所有当前处于睡眠模式等待的线程将一一Value重试。valueFactory这意味着,例如,如果 100 个线程Value同时请求,并且valueFactory在失败前需要 1 秒,则将valueFactory被调用 100 次,并且列表中的最后一个线程将在获取异常之前等待 100 秒。
可以说,更好的行为是将 的错误传播valueFactory到当前正在等待的所有线程。这样,任何线程等待响应的时间都不会超过单次valueFactory调用的持续时间。下面是具有这种行为的类的实现LazyWithRetry<T>:
/// <summary>
/// Represents the result of an action that is invoked lazily on demand, and can be
/// retried as many times as needed until it succeeds, while enforcing a
/// non-overlapping execution policy.
/// </summary>
/// <remarks>
/// In case the action is successful, it is never invoked again. In case of failure
/// the error is propagated to the invoking thread, as well as to all other threads
/// that are currently waiting for the result. The error is not cached. The action
/// will be invoked again when the next thread requests the result, repeating the
/// same pattern.
/// </remarks>
public class LazyWithRetry<T>
{
private volatile Lazy<T> _lazy;
public LazyWithRetry(Func<T> valueFactory)
{
ArgumentNullException.ThrowIfNull(valueFactory);
T GetValue()
{
try { return valueFactory(); }
catch { _lazy = new(GetValue); throw; }
}
_lazy = new(GetValue);
}
public T Value => _lazy.Value;
}
Run Code Online (Sandbox Code Playgroud)
可以在此处LazyWithRetry<T>找到该类的演示。以下是该演示的示例输出:
/// <summary>
/// Represents the result of an action that is invoked lazily on demand, and can be
/// retried as many times as needed until it succeeds, while enforcing a
/// non-overlapping execution policy.
/// </summary>
/// <remarks>
/// In case the action is successful, it is never invoked again. In case of failure
/// the error is propagated to the invoking thread, as well as to all other threads
/// that are currently waiting for the result. The error is not cached. The action
/// will be invoked again when the next thread requests the result, repeating the
/// same pattern.
/// </remarks>
public class LazyWithRetry<T>
{
private volatile Lazy<T> _lazy;
public LazyWithRetry(Func<T> valueFactory)
{
ArgumentNullException.ThrowIfNull(valueFactory);
T GetValue()
{
try { return valueFactory(); }
catch { _lazy = new(GetValue); throw; }
}
_lazy = new(GetValue);
}
public T Value => _lazy.Value;
}
Run Code Online (Sandbox Code Playgroud)
下面是使用一个AtomicLazy<T>或一个SimpleLazy<T>类时同一演示的示例输出:
20:13:12.283 [4] > Worker #1 before requesting value
20:13:12.303 [4] > **Value factory invoked
20:13:12.380 [5] > Worker #2 before requesting value
20:13:12.481 [6] > Worker #3 before requesting value
20:13:12.554 [4] > --Worker #1 failed: Oops! (1)
20:13:12.555 [5] > --Worker #2 failed: Oops! (1)
20:13:12.555 [6] > --Worker #3 failed: Oops! (1)
20:13:12.581 [7] > Worker #4 before requesting value
20:13:12.581 [7] > **Value factory invoked
20:13:12.681 [8] > Worker #5 before requesting value
20:13:12.781 [9] > Worker #6 before requesting value
20:13:12.831 [7] > --Worker #4 failed: Oops! (2)
20:13:12.831 [9] > --Worker #6 failed: Oops! (2)
20:13:12.832 [8] > --Worker #5 failed: Oops! (2)
20:13:12.881 [10] > Worker #7 before requesting value
20:13:12.881 [10] > **Value factory invoked
20:13:12.981 [11] > Worker #8 before requesting value
20:13:13.081 [12] > Worker #9 before requesting value
20:13:13.131 [10] > --Worker #7 received value: 3
20:13:13.131 [11] > --Worker #8 received value: 3
20:13:13.132 [12] > --Worker #9 received value: 3
20:13:13.181 [13] > Worker #10 before requesting value
20:13:13.181 [13] > --Worker #10 received value: 3
20:13:13.182 [1] > Finished
Run Code Online (Sandbox Code Playgroud)
LazyWithRetry<T>可以在本答案的第五版中找到该类的更高级(内存优化)实现。
不幸的是,这是错误的解决 请忽略它并使用tsul答案.只有当你想调试它并发现bug时才离开它.
这是使用tsul SimpleLazy的工作解决方案(与工厂并发缓存):https://dotnetfiddle.net/Y2GP2z
我最终得到了以下解决方案:包装Lazy以模仿与Lazy相同的功能但没有异常缓存.
这是LazyWithoutExceptionsCaching类:
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;
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
完整的工作示例(在这里FIDDLE):
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);
}
});
}
}
}
Run Code Online (Sandbox Code Playgroud)
为此很难使用内置的 Lazy:您应该将您的LazyWithoutExceptionCaching.Value getter包装在一个锁中。但这使得内置的使用变得Lazy多余:您将在Lazy.Valuegetter 中拥有不必要的锁。
最好编写自己的 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());
}
}
}
Run Code Online (Sandbox Code Playgroud)
PS 也许我们会在此问题关闭后内置此功能。
实际上,这个功能是有争议的:https : //github.com/dotnet/corefx/issues/32337
等待,我使用 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);
}
Run Code Online (Sandbox Code Playgroud)