.net 无< T>异常的懒惰缓存

zour9fqk  于 2023-05-19  发布在  .NET
关注(0)|答案(7)|浏览(105)

是否存在无异常缓存的System.Lazy<T>?或者是另一个懒惰的多线程初始化和缓存的好解决方案?
下面是一个程序(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>.只有一次异常被抛出,所以只有一次我们应该看到“Dark side happened..."。在<0的范围内有5个键; 4>所以只有5个“Lightsaber produced”消息应该在控制台上。我们应该看到6次消息“LightsaberProvider.GetFor jedi:因为每个键一次+键“2”一次失败。
lmyy7pcs

lmyy7pcs1#

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

cnwbcb6i2#

很难使用built-in Lazy来实现:你应该把你的LazyWithoutExceptionCaching.Value getter包在一个锁里。但是这使得使用内置的Lazy变得多余:你会在Lazy.Value getter里面有不必要的锁。
最好编写自己的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());
        }
    }
}

P.S.当this issue关闭时,我们可能会内置此功能。

1zmg4dgp

1zmg4dgp3#

不幸的是,这是错误的解决方案!请忽略它并使用tul回答。只有当你想调试它并发现bug时才留下它。
下面是使用tsulSimpleLazy的工作解决方案(使用工厂的并发缓存):https://dotnetfiddle.net/Y2GP2z
我最终得到了以下解决方案: Package 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;
            }
        }
    }
}

完整工作示例(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);
                }
            });
        }
    }
}
v09wglhw

v09wglhw4#

vernoutsul(分别为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;
}

LazyWithRetry<T>类的演示可以在here中找到。下面是此演示的示例输出:

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

下面是使用AtomicLazy<T>SimpleLazy<T>类时相同演示的示例输出:

20:13:38.192  [4] > Worker #1 before requesting value
20:13:38.212  [4] > **Value factory invoked
20:13:38.290  [5] > Worker #2 before requesting value
20:13:38.390  [6] > Worker #3 before requesting value
20:13:38.463  [5] > **Value factory invoked
20:13:38.463  [4] > --Worker #1 failed: Oops! (1)
20:13:38.490  [7] > Worker #4 before requesting value
20:13:38.590  [8] > Worker #5 before requesting value
20:13:38.690  [9] > Worker #6 before requesting value
20:13:38.713  [5] > --Worker #2 failed: Oops! (2)
20:13:38.713  [6] > **Value factory invoked
20:13:38.791 [10] > Worker #7 before requesting value
20:13:38.891 [11] > Worker #8 before requesting value
20:13:38.963  [6] > --Worker #3 received value: 3
20:13:38.964  [8] > --Worker #5 received value: 3
20:13:38.964  [7] > --Worker #4 received value: 3
20:13:38.964  [9] > --Worker #6 received value: 3
20:13:38.964 [10] > --Worker #7 received value: 3
20:13:38.964 [11] > --Worker #8 received value: 3
20:13:38.991 [12] > Worker #9 before requesting value
20:13:38.991 [12] > --Worker #9 received value: 3
20:13:39.091 [13] > Worker #10 before requesting value
20:13:39.091 [13] > --Worker #10 received value: 3
20:13:39.091  [1] > Finished
iyr7buue

iyr7buue5#

正如我在注解中提到的,您可以通过使用TPL libraryTask对象来简化代码:

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);
}

然后,你可以像这样wait for the result这个任务:

resultTask.Wait();

执行此操作将缓存具体x的操作结果。如果任务运行正常,则可以检查Result属性。如果任务失败,Exception属性将存储带有内部实际异常的AggregateExceptionResult已缓存,不会重新计算。如果任务失败,它将在调用Result属性或它的其他一些阻塞方法时throw它的异常。如果你需要不同参数的结果,你应该创建一个新任务
我鼓励你检查这个库,因为你会保存你的时间来重新发明轮子:)你也会得到一些开箱即用的功能,如多线程,异常处理,任务取消等等。祝你的项目好运:)

fdx2calv

fdx2calv6#

更好的方法:

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);
        }
    }
}
0yg35tkg

0yg35tkg7#

基于@piotrwest创建了这个类作为改进!

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 );
        }
    }
}

使用LazyThreadSafetyMode.PublicationOnly配置Lazy示例可以重试,直到获得所需的值,但它也允许同时调用多个Create函数。为了应对这种机制,我添加了一个引用计数器,以允许同时只调用一个valueFactory。只有在可以从Value属性管理故障的情况下才应考虑使用此选项。

相关问题