Lazy <T>无异常缓存

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选项类似,但没有例外缓存.

总而言之,必须满足以下技术要求:

  • 我们想要一个线程安全的缓存
  • 缓存是键值缓存.让我们简化它,键是字符串的类型,值也是字符串的类型
  • 生产一个项目是昂贵的 - 因此生产必须由一个且只有一个线程开始给定密钥.密钥"a"的生产不会阻止密钥"b"的生产
  • 如果生产成功结束 - 我们想要缓存生产的项目
  • 如果在抛出生产异常期间 - 我们希望将异常传递给调用者.呼叫者的责任是决定重试/放弃/记录.异常未缓存 - 下次调用此项目的缓存将启动项目生成.

在我的例子中:

  • 我们有LightsabersCache,LightsabersCache.GetLightsaber方法获取给定键的值
  • LightsaberProvider只是一个虚拟提供者.它模仿生产性质:生产很昂贵(2秒),有时(在这种情况下只是第一次,对于key ="2")异常被抛出
  • 程序启动15个线程,每个线程尝试10次以从范围<0; 4>获取值.只抛出一次异常,所以只有一次我们应该看到"黑暗面发生了......".范围<0; 4>中有5个键,因此控制台上只应显示5个"Lightsaber"消息.我们应该看到6次消息"LightsaberProvider.GetFor jedi:x",因为每个键一次+一次键"2"失败.

The*_*ias 7

vernoutsulAtomicLazy<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>可以在本答案的第五版中找到该类的更高级(内存优化)实现。


pio*_*est 5

不幸的是,这是错误的解决 请忽略它并使用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.你可能也需要锁定它. (3认同)
  • 是的,但锁定应该在try/catch周围,而不是在其中.如果两个线程同时尝试,则它们可以获得两个不同的值,因此它不是线程安全的. (3认同)
  • 不幸的是,它确实不是线程安全的:https://dotnetfiddle.net/Q4oYi8 它导致 valueFactory 即使成功也被多次调用,这颠覆了 Lazy&lt;T&gt; 的所有想法。 (2认同)
  • 我明白了,你们是完全正确的人。我已更改已接受的答案,并编辑了说明问题的答案。 (2认同)

tsu*_*sul 5

为此很难使用内置的 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 也许我们会在此问题关闭后内置此功能。


Orw*_*wel 5

实际上,这个功能是有争议的: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)