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

时间:2015-12-30 12:24:37

标签: c# .net-4.0 thread-safety

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

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

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

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

因此 LazyThreadSafetyMode.ExecutionAndPublication 的关键是,如果&#34;首先去&#34;在创建失败时,可以在以后重新尝试。

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

N.B。对于一个用例,想象一下&#34;创建&#34;可能是昂贵的并且容易出现间歇性错误,包括例如从远程服务器获取大量数据。我不想进行多次并发尝试来获取数据,因为他们可能都会失败或全部成功。但是,如果它们失败了,我希望以后能够重试。

5 个答案:

答案 0 :(得分:4)

  

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

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

更新:

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

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

答案 1 :(得分:2)

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

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

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

答案 2 :(得分:1)

部分受Darin's answer的启发,但试图让这个“等待线程的队列被异常”和“再试一次”功能工作:

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

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

答案 3 :(得分:1)

我尝试的Darin's updated answer版本没有竞争条件我pointed out ...警告,我不完全确定这最终完全没有种族条件。

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>观察到{{1} (未来的请求都会成功)。

  • Lazy<T> = 0
  • t1:在waitersInterlocked.Decrement = 1)之前运行
  • t2:进入并运行到waitersInterlocked.Increment = 1)之前
  • t1:执行waiters并准备覆盖(Interlocked.Decrement = 0)
  • t2:在waitersInterlocked.Decrement = 1)
  • 之前运行
  • t1:用新的waiters覆盖lazy(称之为lazy1)(waiters = 1)
  • t3:lazy1waiters = 2)
  • 进来并阻止
  • t2:执行Interlocked.Decrementwaiters = 1)
  • t3:获取并返回lazy1的值(waiters现在无关紧要)
  • t2:重新抛出异常

我无法提出一系列事件,这些事件会导致比#34更糟糕的事情;这个线程在另一个线程产生成功结果后引发异常&#34;。

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

我还提醒自己为什么我脑子里有这个东西说&#34;低锁并发编程很难,只需使用C#lock语句!!!&# 34;我一直在写原始答案。

Update3:刚刚更改了Update2中的一些文本,指出了使volatile成为必要的实际情况 - 这里使用的Interlocked操作显然是在今天的重要CPU架构上全屏实现的,而不是我原来只是假装的半栅栏,所以volatile保护了比我原先想象的更窄的部分。

答案 4 :(得分:1)

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

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