猿问

具有不同线程安全模式的System.Lazy <T>

.NET 4.0的System.Lazy <T>类通过枚举LazyThreadSafetyMode提供了三种线程安全模式,我将其总结为:

  • LazyThreadSafetyMode.None - 不是线程安全的。

  • LazyThreadSafetyMode.ExecutionAndPublication - 只有一个线程同时将尝试创造潜在价值。创建成功后,所有等待线程将获得相同的值。如果在创建过程中发生未处理的异常,则将在每个等待的线程上将其重新抛出,在每次后续访问基础值的尝试中将对其进行缓存和重新抛出。

  • LazyThreadSafetyMode.PublicationOnly - 多个并发线程将尝试创造潜在价值,但第一个成功将决定传递给所有线程值。如果在创建过程中发生未处理的异常,则不会对其进行缓存,并且并发访问和随后尝试访问基础值的尝试将重试创建,并且可能会成功。

我想要一个延迟初始化的值,该值遵循略有不同的线程安全规则,即:

只有一个并发线程将尝试创建基础值。创建成功后,所有等待线程将获得相同的值。如果在创建过程中发生未处理的异常,它将在每个等待的线程上重新抛出,但不会被缓存,并且随后尝试访问基础值的尝试将重试创建,并且可能会成功。

因此,与LazyThreadSafetyMode.ExecutionAndPublication的主要区别在于,如果创建时“先行尝试”失败,则可以在以后重新尝试。

是否存在提供这些语义的现有(.NET 4.0)类,还是我必须自己滚动?如果我自己动手,是否存在一种聪明的方法来重用实现中的现有Lazy <T>以避免显式的锁定/同步?


注意:对于一个用例,想象一下“创建”可能是昂贵的并且容易出现间歇性错误,例如涉及从远程服务器获取大量数据。我不想进行多次并发尝试来获取数据,因为它们很可能全部失败或全部成功。但是,如果它们失败了,我希望稍后再试。


慕妹3146593
浏览 921回答 3
3回答

白板的微信

我尝试的是达林更新后的答案版本,但没有我指出的比赛条件...警告,我不确定这最终是否完全摆脱了比赛条件。private static int waiters = 0;private static volatile Lazy<object> lazy = new Lazy<object>(GetValueFromSomewhere);public static object Value{&nbsp; &nbsp; get&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; Lazy<object> currLazy = lazy;&nbsp; &nbsp; &nbsp; &nbsp; if (currLazy.IsValueCreated)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return currLazy.Value;&nbsp; &nbsp; &nbsp; &nbsp; Interlocked.Increment(ref waiters);&nbsp; &nbsp; &nbsp; &nbsp; try&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return lazy.Value;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // just leave "waiters" at whatever it is... no harm in it.&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; catch&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (Interlocked.Decrement(ref waiters) == 0)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; lazy = new Lazy<object>(GetValueFromSomewhere);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throw;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}更新:我以为发布此消息后发现了比赛情况。该行为实际上应该是可以接受的,只要您对一个罕见的情况感到满意,即Lazy<T>在另一个线程已经从成功的快速返回之后,某个线程抛出了一个从慢速观察到的异常Lazy<T>(将来的请求将全部成功)。waiters = 0t1:一直运行到Interlocked.Decrement(waiters= 1)之前t2:进入并运行到Interlocked.Increment(waiters= 1)之前t1:进行Interlocked.Decrement并准备覆盖(waiters= 0)t2:一直运行到Interlocked.Decrement(waiters= 1)之前t1:lazy用新的覆盖(称为lazy1)(waiters= 1)t3:进入并在lazy1(waiters= 2)处阻止t2:是否执行Interlocked.Decrement(waiters= 1)t3:从lazy1(waiters现在不相关)获取并返回值t2:抛出异常我无法提出一系列导致比“该线程在另一个线程产生成功结果之后引发异常”更糟糕的事情的事件。Update2:声明lazy为volatile确保所有读者立即都能看到受保护的覆盖。有些人(包括我自己在内)看到volatile并立即想到“好吧,可能是使用不正确”,他们通常是正确的。这就是我在这里使用它的原因:在上面示例中的事件序列中,t3仍然可以读取旧的,lazy而不是lazy1如果它位于lazy.Valuet1修改lazy为包含的那一刻之前lazy1。 volatile防止这种情况,以便下次尝试可以立即开始。我还提醒自己,为什么我脑子里有这样的话:“低锁并发编程很难,只需使用C#lock语句!!!” 我一直在写原始答案。Update3:只是更改了Update2中的一些文本,指出了volatile必要的实际情况- Interlocked此处使用的操作显然是在当今重要的CPU架构上全屏实现的,而不是我最初只是假设的半屏实现的,因此volatile保护的范围比我原来想象的要窄得多。

长风秋雁

只有一个并发线程将尝试创建基础值。创建成功后,所有等待线程将获得相同的值。如果在创建过程中发生未处理的异常,它将在每个等待的线程上重新抛出,但不会被缓存,并且随后尝试访问基础值的尝试将重试创建,并且可能会成功。由于Lazy不支持该功能,因此您可以尝试自行滚动:private static object syncRoot = new object();private static object value = null;public static object Value{&nbsp; &nbsp; get&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; if (value == null)&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; lock (syncRoot)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (value == null)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // Only one concurrent thread will attempt to create the underlying value.&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // And if `GetTheValueFromSomewhere` throws an exception, then the value field&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // will not be assigned to anything and later access&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // to the Value property will retry. As far as the exception&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // is concerned it will obviously be propagated&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // to the consumer of the Value getter&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value = GetTheValueFromSomewhere();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; return value;&nbsp; &nbsp; }}更新:为了满足您对传播到所有等待读取器线程的相同异常的要求:private static Lazy<object> lazy = new Lazy<object>(GetTheValueFromSomewhere);public static object Value{&nbsp; &nbsp; get&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; try&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return lazy.Value;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; catch&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // We recreate the lazy field so that subsequent readers&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // don't just get a cached exception but rather attempt&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // to call the GetTheValueFromSomewhere() expensive method&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // in order to calculate the value again&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; lazy = new Lazy<object>(GetTheValueFromSomewhere);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // Re-throw the exception so that all blocked reader threads&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // will get this exact same exception thrown.&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throw;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}
随时随地看视频慕课网APP
我要回答