Lazy(Func)的异常缓存问题及解决方案

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

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

问题

但是程序运行一段时间后出现了诡异的情况:出现一次异常后,程序不能自动恢复,一直抛出异常,直到程序重启。这就像苏伊士运河中搁浅的船只,自己无法走出来了,必须有人去推它一把。

Lazy,Func的异常缓存问题及解决方案

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

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

需要详细了解的小伙伴可以打开这个地址细品:https://docs.microsoft.com/en-us/dotnet/api/system.lazy-1.-ctor?view=net-5.0#System_Lazy_1__ctor_System_Func__0__

官方还提供了一个例子可以验证异常缓存的问题,这里由于篇幅原因也不粘贴了,想看的朋友可以去试试。

解决方案

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

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

来看几个解决方案:

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

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

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

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

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

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

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

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

哪个适合自己,还需仔细斟酌。