没有异常缓存的 Lazy<T>

17
有没有不带异常缓存的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);
                }
            });
        }
    }
}

基本上,我希望缓存已生产的光剑,但生产它们既昂贵又棘手 - 有时可能会发生异常。我想为给定的jedi仅允许一个生产者,但当抛出异常时 - 我想让另一个生产者再试一次。因此,期望的行为类似于System.Lazy ,具有LazyThreadSafetyMode.ExecutionAndPublication选项,但没有异常缓存。
总之,必须满足以下技术要求:
- 我们希望拥有线程安全的缓存。 - 缓存是键值缓存。让我们简化它,键的类型为字符串类型,值也是字符串类型。 - 生产一个项目非常昂贵 - 因此对于给定的键,生产必须由一个且仅有一个线程开始。键“a”的生产不会阻止键“b”的生产。 - 如果生产成功结束 - 我们希望缓存生成的项目。 - 如果在生产过程中抛出异常 - 我们希望将异常传递给调用者。调用者的责任是决定重试/放弃/记录。异常不会被缓存 - 对于此项的下一次缓存调用将启动该项的生产。
在我的例子中:
- 我们有LightsabersCache,LightsabersCache.GetLightsaber方法获取给定键的值。 - LightsaberProvider只是一个虚拟提供者。它模仿生产性质:生产是昂贵的(2秒),有时(在此情况下仅第一次,对于key =“2”)会抛出异常。 - 该程序启动15个线程,每个线程尝试10次从范围<0;4>获取值。仅有一次异常被抛出,因此我们应该只看到一次“Dark side happened...”。在<0;4>范围内有5个键,因此控制台上应只有5个“Lightsaber produced”的消息。我们应该看到6次消息“LightsaberProvider.GetFor jedi:x”,因为每个键一次+键“2”失败一次。

您可以在错误处理程序上使用续行符切换到Task - VMAtm
你能进一步阐述吗? - piotrwest
请参考 https://dev59.com/klsW5IYBdhLWcg3w2aKK#42567351。 - mjwills
那是一个不同的使用情况。而且,你的解决方案不太可测试,并且不能同时存在两个缓存。 - piotrwest
7个回答

10
实际上,这个功能正在进行讨论:引入第四种LazyThreadSafetyMode类型:ThreadSafeValueOnly
为了等待,我使用了Marius Gundersen的这个优雅的实现:Lazy(和AsyncLazy)在处理异常方面表现不佳
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);
}

8

对于这个问题,使用内置的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. 当此问题被关闭时,也许我们会内置这个功能。


6

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>类的演示。以下是此演示的样本输出:

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

1
当递归时,使用本地方法而不是委托可以简化代码;无需在定义之前声明空委托。 - Servy
1
还要注意,这里大多数其他答案在成功构建值后仍然保留对原始委托的引用,而这个实现不会,因此如果有闭合对象,此实现在构建对象后不会保留对它们的引用,而其他答案则可以在某个地方将字段分配为null来修复它,但这种方法更干净,因为根本没有该字段。 - Servy

4

不幸的是,这个解决方案是错误的!请忽略它,使用 tsul 的答案。只有在你想要调试并发现错误时才会保留它。

这里是工作解决方案(具有工厂的并发缓存)和tsul SimpleLazy:https://dotnetfiddle.net/Y2GP2z


我最终采用了以下解决方案:包装延迟加载(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;
            }
        }
    }
}

完整的工作示例(在这里查看):

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

5
很遗憾,这段代码不是线程安全的,因为两个线程可以同时写入 _lazy。你可能还需要在其周围添加一个锁。 - Marius
这不就是 Lazy<T> 的目的吗?在我看来,你不需要锁。 - piotrwest
4
是的,但是锁应该放在try/catch之外,而不是里面。如果两个线程同时尝试获取值,它们可能获得不同的结果,因此这不是线程安全的。 - Marius
2
不幸的是,它真的不是线程安全的: https://dotnetfiddle.net/Q4oYi8 即使成功,它也会导致valueFactory被调用多次,这颠覆了Lazy<T>的所有想法。 - tsul
2
我明白了,你们说得完全正确。我已经更改了接受的答案,并编辑了这个回答来说明问题。 - piotrwest

0

正如我在评论中提到的那样,您可以通过使用TPL库Task对象来简化您的代码:

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

之后,您可以像这样等待此任务的结果

resultTask.Wait();

制作这个将会为具体的x缓存操作结果。如果任务运行正确,您可以检查Result属性。如果任务失败,Exception属性将存储带有内部实际异常的AggregateExceptionResult被缓存并且不会重新计算。如果任务失败,它将在调用Result属性或其它阻塞方法时throw其异常。如果您需要不同参数的结果,则应创建新任务

我鼓励您检查此库,因为您将节省时间,避免重复造轮子 :) 同时,您将获得一些开箱即用的功能,如多线程、异常处理、任务取消等等。祝您的项目好运 :)


我理解用Task替换Lazy的概念。但是,你如何确切地开始一个任务呢?当使用工厂方法调用GetOrAdd方法时,ConcurrentDictionary可能会创建冗余值。还有一件事 - 即使你设法阻止多次调用工厂方法,你如何替换抛出异常结果的任务呢?总之,缓存结果的想法很好,但我认为在这种情况下,使用缓存和并发字典不起作用。请随意查看我的答案中的LazyWithoutExceptionsCaching的预期结果。 - piotrwest
我会更新我的回答,但是想要提醒您不要使用 TryGetValue - 您可以直接调用 GetOrAdd - 它的工作方式完全相同。 - VMAtm
好的,我会更新问题并解释一下它应该如何工作。它有相当合理的技术要求,知道是否可以更好地解决问题会很有帮助。 - piotrwest
@piotrwest 我看到了你的添加,是的,我的方法不适用于它们 - 你将不得不为任务创建一个字典,而不是结果,这可能非常复杂。 - VMAtm
抱歉之前没有明确说明。无论如何,如果您能想到比我的更好的解决方案(似乎我的解决方案运行良好)-请告诉我。 - piotrwest
显示剩余3条评论

-2

基于 @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属性管理失败的情况下考虑使用此选项。


-2
更好的方式:
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);
        }
    }
}

我不这么认为。虽然 LazyInitializer.EnsureInitialized 仅会分配一次 instance,但它可能多次执行 valueFactory(),这实际上破坏了原来的目的。"如果多个线程同时访问此方法,可能会创建多个 T 的实例,但只有一个实例将被存储到目标并返回" - Phil B

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接