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

.NET 4.0的System.Lazy 类通过枚举LazyThreadSafetyMode提供三种线程安全模式,我将其概括为:

  • LazyThreadSafetyMode.None不是线程安全的。
  • LazyThreadSafetyMode.ExecutionAndPublication只有一个并发线程将尝试创建基础值。 成功创建后,所有等待的线程将获得相同的值。 如果在创建期间发生未处理的exception,则将在每个等待的线程上重新抛出它,在每次后续尝试访问基础值时进行缓存和重新抛出。
  • LazyThreadSafetyMode.PublicationOnly多个并发线程将尝试创建基础值,但第一个成功将确定传递给所有线程的值。 如果在创建期间发生未处理的exception,则不会对其进行高速缓存,并且后续尝试访问基础值将重新尝试创建并可能成功。

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

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

因此,与LazyThreadSafetyMode.ExecutionAndPublication的关键不同之在于,如果创建时“先行”失败,可以在以后重新尝试。

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


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

我尝试了Darin的更新答案 ,没有我指出的竞争条件……警告,我不完全确定这最终完全没有竞争条件。

 private static int waiters = 0; private static volatile Lazy lazy = new Lazy(GetValueFromSomewhere); public static object Value { get { Lazy currLazy = lazy; if (currLazy.IsValueCreated) return currLazy.Value; Interlocked.Increment(ref waiters); try { return lazy.Value; // just leave "waiters" at whatever it is... no harm in it. } catch { if (Interlocked.Decrement(ref waiters) == 0) lazy = new Lazy(GetValueFromSomewhere); throw; } } } 

更新:我发现在发布此消息后我发现了一个竞争条件。 这种行为实际上应该是可以接受的,只要你可以接受一个可能很少的情况,其中一些线程抛出exception,它从另一个线程已经从成功的快速Lazy (未来)返回后从慢速Lazy观察到请求都会成功)。

  • waiters = 0
  • t1:在Interlocked.Decrementwaiters = 1)之前运行
  • t2:进入并运行到Interlocked.Incrementwaiters = 1)之前
  • t1:它的Interlocked.Decrement并准备覆盖( waiters = 0)
  • t2:在Interlocked.Decrementwaiters = 1)之前运行
  • t1:用新的覆盖lazy (称之为lazy1 )( waiters = 1)
  • t3:进入并阻止lazy1waiters = 2)
  • t2:它的Interlocked.Decrementwaiters = 1)
  • t3:从lazy1获取并返回值( waiters现在无关紧要)
  • t2:重新抛出exception

我无法想出一系列事件会导致比“另一个线程产生成功结果后该线程抛出exception”更糟糕的事件。

Update2:将lazy声明为volatile以确保所有读者立即看到保护覆盖。 有些人(包括我自己)看到volatile并立即认为“好吧,这可能被错误地使用了”,而且他们通常是正确的。 这就是我在这里使用它的原因:在上面例子中的事件序列中,如果t1在读取lazy.Value之前定位,则t3仍然可以读取旧的lazy而不是lazy 。值t1修改lazy以包含lazy1volatile可以防止这种情况发生,以便下一次尝试可以立即开始。

我还提醒自己,为什么我在脑后说“低锁并发编程很难,只需使用C# lock语句!!!” 我写这个原始答案的整个过程。

Update3:刚刚更改了Update2中的一些文本,指出了使得volatile变得必要的实际情况 – 这里使用的Interlocked操作显然是在今天重要的CPU架构上实现的全栅栏而不是半栅栏,因为我最初只是排序假设,如此volatile保护比我原先想象的更窄的部分。

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

由于Lazy不支持,你可以尝试自己滚动它:

 private static object syncRoot = new object(); private static object value = null; public static object Value { get { if (value == null) { lock (syncRoot) { if (value == null) { // Only one concurrent thread will attempt to create the underlying value. // And if `GetTheValueFromSomewhere` throws an exception, then the value field // will not be assigned to anything and later access // to the Value property will retry. As far as the exception // is concerned it will obviously be propagated // to the consumer of the Value getter value = GetTheValueFromSomewhere(); } } } return value; } } 

更新:

为了满足您对传播到所有等待读者线程的相同exception的要求:

 private static Lazy lazy = new Lazy(GetTheValueFromSomewhere); public static object Value { get { try { return lazy.Value; } catch { // We recreate the lazy field so that subsequent readers // don't just get a cached exception but rather attempt // to call the GetTheValueFromSomewhere() expensive method // in order to calculate the value again lazy = new Lazy(GetTheValueFromSomewhere); // Re-throw the exception so that all blocked reader threads // will get this exact same exception thrown. throw; } } } 

懒惰不支持这个。 这是Lazy的一个设计问题,因为exception“缓存”意味着该延迟实例不会永远提供真正的值。 由于网络问题等瞬态错误,这可能会导致应用程序永久关闭。 那时通常需要人为干预。

我打赌这个地雷存在于不少.NET应用程序中……

你需要写自己的懒惰才能做到这一点。 或者,为此打开CoreFx Github问题。

部分受到Darin的回答的启发,但试图让这个“等待线程的队列被exception”和“再试一次”function工作:

 private static Task _fetcher = null; private static object _value = null; public static object Value { get { if (_value != null) return _value; //We're "locking" then var tcs = new TaskCompletionSource(); var tsk = Interlocked.CompareExchange(ref _fetcher, tcs.Task, null); if (tsk == null) //We won the race to set up the task { try { var result = new object(); //Whatever the real, expensive operation is tcs.SetResult(result); _value = result; return result; } catch (Exception ex) { Interlocked.Exchange(ref _fetcher, null); //We failed. Let someone else try again in the future tcs.SetException(ex); throw; } } tsk.Wait(); //Someone else is doing the work return tsk.Result; } } 

我有点担心 – 有人能看到任何明显的比赛,它会以一种不明显的方式失败吗?

这样的事情可能会有所帮助:

 using System; using System.Threading; namespace ADifferentLazy { ///  /// Basically the same as Lazy with LazyThreadSafetyMode of ExecutionAndPublication, BUT exceptions are not cached ///  public class LazyWithNoExceptionCaching { private Func valueFactory; private T value = default(T); private readonly object lockObject = new object(); private bool initialized = false; private static readonly Func ALREADY_INVOKED_SENTINEL = () => default(T); public LazyWithNoExceptionCaching(Func valueFactory) { this.valueFactory = valueFactory; } public bool IsValueCreated { get { return initialized; } } public T Value { get { //Mimic LazyInitializer.EnsureInitialized()'s double-checked locking, whilst allowing control flow to clear valueFactory on successful initialisation if (Volatile.Read(ref initialized)) return value; lock (lockObject) { if (Volatile.Read(ref initialized)) return value; value = valueFactory(); Volatile.Write(ref initialized, true); } valueFactory = ALREADY_INVOKED_SENTINEL; return value; } } } }