zoukankan      html  css  js  c++  java
  • Lazy(Func<T>)的异常缓存问题

    Lazy可以提供多线程环境下的安全保障,但是用不好也是会跳到坑里。

    我这里使用Lazy<t>(Func<T>)来创建一个Lazy实例,然后在需要的地方访问它的Value属性,它可以保证在多线程环境下Func<T>仅执行一次,这看起来十分的美好:需要的时候执行,并且仅执行一次,再翻译下就是延迟加载,线程安全,资源消耗少。

    问题

    但是程序运行一段时间后出现了诡异的情况:出现一次异常后,程序不能自动恢复,一直抛出异常,直到程序重启,而出现异常的地方就在Func<T>中。

    所有的好冥冥之中都是有代价的,查阅官方文档,发现Lazy会缓存异常。

    https://docs.microsoft.com/en-us/dotnet/api/system.lazy-1.-ctor?view=net-5.0#System_Lazy_1__ctor_System_Func__0__

    Lazy<T>(Func<T>) 等同于 Lazy<T>(Func<T>, true) 或者 Lazy<T>(Func<T>,LazyThreadSafetyMode.ExecutionAndPublication),后边这两个构造函数的第二个参数的意思是在多线程环境下,委托只执行1次,使用这次的执行结果作为Lazy的值,同时如果委托中发生任何异常,都会被缓存下来。

    官方还提供了一个例子可以验证异常缓存的问题,粘贴到这里:

    using System;
    using System.Threading;
    
    class Program
    {
        static Lazy<LargeObject> lazyLargeObject = null;
    
        static LargeObject InitLargeObject()
        {
            return new LargeObject();
        }
    
        static void Main()
        {
            // The lazy initializer is created here. LargeObject is not created until the
            // ThreadProc method executes.
            lazyLargeObject = new Lazy<LargeObject>(InitLargeObject);
    
            // The following lines show how to use other constructors to achieve exactly the
            // same result as the previous line:
            //lazyLargeObject = new Lazy<LargeObject>(InitLargeObject, true);
            //lazyLargeObject = new Lazy<LargeObject>(InitLargeObject, LazyThreadSafetyMode.ExecutionAndPublication);
    
            Console.WriteLine(
                "
    LargeObject is not created until you access the Value property of the lazy" +
                "
    initializer. Press Enter to create LargeObject.");
            Console.ReadLine();
    
            // Create and start 3 threads, each of which tries to use LargeObject.
            Thread[] threads = { new Thread(ThreadProc), new Thread(ThreadProc), new Thread(ThreadProc) };
            foreach (Thread t in threads)
            {
                t.Start();
            }
    
            // Wait for all 3 threads to finish. (The order doesn't matter.)
            foreach (Thread t in threads)
            {
                t.Join();
            }
    
            Console.WriteLine("
    Press Enter to end the program");
            Console.ReadLine();
        }
    
        static void ThreadProc(object state)
        {
            try
            {
                LargeObject large = lazyLargeObject.Value;
    
                // IMPORTANT: Lazy initialization is thread-safe, but it doesn't protect the
                //            object after creation. You must lock the object before accessing it,
                //            unless the type is thread safe. (LargeObject is not thread safe.)
                lock(large)
                {
                    large.Data[0] = Thread.CurrentThread.ManagedThreadId;
                    Console.WriteLine("Initialized by thread {0}; last used by thread {1}.",
                        large.InitializedBy, large.Data[0]);
                }
            }
            catch (ApplicationException aex)
            {
                Console.WriteLine("Exception: {0}", aex.Message);
            }
        }
    }
    
    class LargeObject
    {
        int initBy = 0;
        public int InitializedBy { get { return initBy; } }
    
        static int instanceCount = 0;
        public LargeObject()
        {
            if (1 == Interlocked.Increment(ref instanceCount))
            {
                throw new ApplicationException("Throw only ONCE.");
            }
    
            initBy = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine("LargeObject was created on thread id {0}.", initBy);
        }
        public long[] Data = new long[100000000];
    }
    
    /* This example produces output similar to the following:
    
    LargeObject is not created until you access the Value property of the lazy
    initializer. Press Enter to create LargeObject.
    
    Exception: Throw only ONCE.
    Exception: Throw only ONCE.
    Exception: Throw only ONCE.
    
    Press Enter to end the program
     */

    解决方案

    在提出解决办法前,需要想一下,为什么会缓存异常?

    因为要保证多线程环境下只执行一次,如果异常了还允许再次执行,就不能保证只执行一次了,而有些程序多次执行是不可行的。

    来看几个解决方案:

    1、不使用Lazy,自己加锁处理。

    出现问题的程序中Lazy内部也是用了锁。

    部分情况下可以用双检锁或则带升级的读写锁,以提高读的性能。

    如果发生异常,可以抛到上层,并且再次获取时会重试执行。

    2、使用Value时如果有异常,则重新给Lazy赋值。

    不过这可能又要求赋值时线程安全。

    3、如果经过评估可以多次创建Value,则可以更改线程安全模式为:LazyThreadSafetyMode.PublicationOnly

    在这种模式下:多线程时每个线程都会创建,但是只使用第一个创建的,同时不缓存异常,异常发生后再次获取时会重新执行。

    哪个适合自己,还需自己选择。

  • 相关阅读:
    Redis-持久化
    Redis-Sort命令
    Redis-ZSet常用命令
    Redis-Hash
    Redis-Set常用命令
    Redis-List常用命令
    Redis-String常用命令
    访问控制
    c++之旅:多态
    c++之旅:类型的强制转换
  • 原文地址:https://www.cnblogs.com/bossma/p/lazy-func-exception-cache-problem.html
Copyright © 2011-2022 走看看