Lazy可以提供多线程环境下的安全保障,但是用不好也是会跳到坑里。
我这里使用Lazy<t>(Func<T>)来创建一个Lazy实例,然后在需要的地方访问它的Value属性,它可以保证在多线程环境下Func<T>仅执行一次,这看起来十分的美好:需要的时候执行,并且仅执行一次,再翻译下就是延迟加载,线程安全,资源消耗少。
问题
但是程序运行一段时间后出现了诡异的情况:出现一次异常后,程序不能自动恢复,一直抛出异常,直到程序重启,而出现异常的地方就在Func<T>中。
所有的好冥冥之中都是有代价的,查阅官方文档,发现Lazy会缓存异常。
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(
"\r\nLargeObject is not created until you access the Value property of the lazy" +
"\r\ninitializer. 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("\r\nPress 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
在这种模式下:多线程时每个线程都会创建,但是只使用第一个创建的,同时不缓存异常,异常发生后再次获取时会重新执行。
哪个适合自己,还需自己选择。
发表评论
相关文章
国内AI资源汇总,AI聊天、AI绘画、AI写作、AI视频、AI设计、AI编程、AI音乐等,国内顺畅访问,无需科学上网。
扫码或点击进入:萤火AI大全
文章分类
最新评论