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

System.Lazy<T> with different thread-safety mode

.NET 4.0 的 System.Lazy<T> class offers three Thread-Safety modes via the enum LazyThreadSafetyMode,我总结为:

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

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

因此与 LazyThreadSafetyMode.ExecutionAndPublication 的主要区别在于,如果 "first go" 在创建时失败,可以稍后重新尝试。

是否有提供这些语义的现有 (.NET 4.0) class,或者我必须自己推出?如果我自己动手,是否有一种聪明的方法可以在实现中重新使用现有的 Lazy 以避免显式 locking/synchronization?


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

懒惰不支持这个。这是 Lazy 的设计问题,因为异常 "caching" 意味着该惰性实例将永远不会提供真正的价值。这可能会由于网络问题等暂时性错误而永久关闭应用程序。然后通常需要人为干预。

我敢打赌这个地雷存在于很多 .NET 应用程序中...

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

Only one concurrent thread will attempt to create the underlying value. On successful creation, all waiting threads will receive the same value. If an unhandled exception occurs during creation, it will be re-thrown on each waiting thread, but it will not be cached and subsequent attempts to access the underlying value will re-try the creation & may succeed.

由于 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;
    }
}

更新:

为了满足您对传播到所有等待 reader 个线程的相同异常的要求:

private static Lazy<object> lazy = new Lazy<object>(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<object>(GetTheValueFromSomewhere);

            // Re-throw the exception so that all blocked reader threads
            // will get this exact same exception thrown.
            throw;
        }
    }
}

部分受到 的启发,但试图让这个 "queue of waiting threads that are inflicted with the exception" 和 "try again" 功能正常工作:

private static Task<object> _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<object>();
        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;
    }
}

虽然我有点担心 - 谁能在这里看到任何明显的比赛,它会以不明显的方式失败?

我对 that doesn't have the race condition I 版本的尝试...警告,我不完全确定这最终完全没有竞争条件。

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;
        }
    }
}

更新:我以为我在发布后发现了竞争条件。这种行为实际上应该是可以接受的,只要你能接受一种可能很少见的情况,即在另一个线程已经从成功的快速 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:获取 returns 来自 lazy1 的值(waiters 现在不相关)
  • t2:重新抛出异常

我想不出比 "this thread threw an exception after another thread yielded a successful result".

更糟糕的一系列事件

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

我还提醒自己为什么我在写原始答案的整个过程中都在说 "low-lock concurrent programming is hard, just use a C# lock statement!!!"。

Update3:刚刚更改了 Update2 中的一些文本,指出需要 volatile 的实际情况——此处使用的 Interlocked 操作显然是 full-fence 在重要的 CPU 今天的体系结构,而不是 half-fence,因为我最初只是 sort-of 假设的,所以 volatile 保护的部分比我原先想象的要窄得多。

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

using System;
using System.Threading;

namespace ADifferentLazy
{
    /// <summary>
    /// Basically the same as Lazy with LazyThreadSafetyMode of ExecutionAndPublication, BUT exceptions are not cached 
    /// </summary>
    public class LazyWithNoExceptionCaching<T>
    {
        private Func<T> valueFactory;
        private T value = default(T);
        private readonly object lockObject = new object();
        private bool initialized = false;
        private static readonly Func<T> ALREADY_INVOKED_SENTINEL = () => default(T);

        public LazyWithNoExceptionCaching(Func<T> 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;
            }
        }
    }
}