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

Mat*_*r82 7 c# .net-4.0 thread-safety

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

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

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

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

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

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


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

Joe*_*nta 1

我尝试了达林更新答案的一个版本,该版本没有我指出的竞争条件......警告,我不完全确定这最终完全没有竞争条件。

private static int waiters = 0;
private static volatile Lazy<object> lazy = new Lazy<object>(GetValueFromSomewhere);
public static object Value
{
    get
    {
        Lazy<object> 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<object>(GetValueFromSomewhere);
            throw;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:我想我在发布此内容后发现了竞争条件。这种行为实际上应该是可以接受的,只要您能接受一种可能罕见的情况,即在Lazy<T>另一个线程已经从成功的快速返回之后,某个线程抛出从慢速观察到的异常Lazy<T>(未来的请求将全部成功)。

  • waiters= 0
  • t1:进入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:获取并返回值lazy1waiters现在不相关)
  • t2:重新抛出异常

我无法想出一系列事件会导致比“此线程在另一个线程产生成功结果后引发异常”更糟糕的情况。

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

我还提醒自己为什么我脑子里有这样的想法:“低锁并发编程很难,只需使用 C#lock语句!!!” 我一直在写原来的答案。

Update3:刚刚更改了Update2中的一些文本,指出了必要的实际情况volatile——Interlocked这里使用的操作显然是在当今重要的CPU架构上实现了全围栏,而不是像我最初假设的那样是半围栏,因此volatile保护的部分比我最初想象的要窄得多。