缓存来自 [n async] 工厂方法的结果,前提是它不抛出

caching the result from a [n async] factory method iff it doesn't throw

更新:在@usr 指出我错误地假设 Lazy<T> 的默认线程安全模式是 LazyThreadSafetyMode.PublicationOnly...

后进行了大量修改

我想通过 async 工厂方法(即 returns Task<T>)懒惰地计算一个值,并在成功时将其缓存。在例外情况下,我希望我可以使用它。但是,我不想成为 Lazy<T> 默认模式下的 the exception caching behavior 的猎物 (LazyThreadSafetyMode.ExecutionAndPublication)

Exception caching: When you use factory methods, exceptions are cached. That is, if the factory method throws an exception the first time a thread tries to access the Value property of the Lazy object, the same exception is thrown on every subsequent attempt. This ensures that every call to the Value property produces the same result and avoids subtle errors that might arise if different threads get different results. The Lazy stands in for an actual T that otherwise would have been initialized at some earlier point, usually during startup. A failure at that earlier point is usually fatal. If there is a potential for a recoverable failure, we recommend that you build the retry logic into the initialization routine (in this case, the factory method), just as you would if you weren’t using lazy initialization.

Stephen Toub has an AsyncLazy class and writeup 好像刚刚好:

public class AsyncLazy<T> : Lazy<Task<T>>
{
    public AsyncLazy(Func<Task<T>> taskFactory) :
        base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
    { }

    public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
}

然而,这实际上与默认行为相同 Lazy<T> - 如果出现问题,将不会重试。

我正在寻找 Lazy<T>(Func<T>, LazyThreadSafetyMode.PublicationOnly)Task<T> 兼容等价物,即它应该按照指定的方式运行:-

Alternative to locking In certain situations, you might want to avoid the overhead of the Lazy object's default locking behavior. In rare situations, there might be a potential for deadlocks. In such cases, you can use the Lazy(LazyThreadSafetyMode) or Lazy(Func, LazyThreadSafetyMode) constructor, and specify LazyThreadSafetyMode.PublicationOnly. This enables the Lazy object to create a copy of the lazily initialized object on each of several threads if the threads call the Value property simultaneously. The Lazy object ensures that all threads use the same instance of the lazily initialized object and discards the instances that are not used. Thus, the cost of reducing the locking overhead is that your program might sometimes create and discard extra copies of an expensive object. In most cases, this is unlikely. The examples for the Lazy(LazyThreadSafetyMode) and Lazy(Func, LazyThreadSafetyMode) constructors demonstrate this behavior.

IMPORTANT

When you specify PublicationOnly, exceptions are never cached, even if you specify a factory method.

是否有任何 FCL、Nito.AsyncEx 或类似的构造可能适合这里?如果做不到这一点,任何人都可以看到一种优雅的方式来控制“正在进行的尝试”位(我同意每个调用者以与 Lazy<T>( ..., (LazyThreadSafetyMode.PublicationOnly) 相同的方式进行自己的尝试有)但仍然有那个和缓存管理整齐地封装?

目前,我正在使用这个:

public class CachedAsync<T>
{
    readonly Func<Task<T>> _taskFactory;
    T _value;

    public CachedAsync(Func<Task<T>> taskFactory)
    {
        _taskFactory = taskFactory;
    }

    public TaskAwaiter<T> GetAwaiter() { return Fetch().GetAwaiter(); }

    async Task<T> Fetch()
    {
        if (_value == null)
            _value = await _taskFactory();
        return _value;
    }
}

虽然它在我的场景中有效(我没有多个触发线程等),但它并不优雅,也不提供两者的线程安全协调

  • 单次尝试正在进行 LazyThreadSafetyMode.ExecutionAndPublication 或者
  • 在 >= 1 次成功之后稳定的结果 a la LazyThreadSafetyMode.PublicationOnly

免责声明: 这是重构 Lazy<T> 的疯狂尝试。它绝不是生产级代码。

我冒昧地查看了 Lazy<T> 源代码并对其进行了一些修改以用于 Func<Task<T>>。我已经将 Value 属性 重构为 FetchValueAsync 方法,因为我们不能在 属性 中等待。您可以自由地使用 Task.Result 阻止 async 操作,这样您仍然可以使用 Value 属性,我不想这样做,因为它可能会导致问题。所以它有点麻烦,但仍然有效。此代码未经过全面测试:

public class AsyncLazy<T>
{
    static class LazyHelpers
    {
        internal static readonly object PUBLICATION_ONLY_SENTINEL = new object();
    }
    class Boxed
    {
        internal Boxed(T value)
        {
            this.value = value;
        }
        internal readonly T value;
    }

    class LazyInternalExceptionHolder
    {
        internal ExceptionDispatchInfo m_edi;
        internal LazyInternalExceptionHolder(Exception ex)
        {
            m_edi = ExceptionDispatchInfo.Capture(ex);
        }
    }

    static readonly Func<Task<T>> alreadyInvokedSentinel = delegate
    {
        Contract.Assert(false, "alreadyInvokedSentinel should never be invoked.");
        return default(Task<T>);
    };

    private object boxed;

    [NonSerialized]
    private Func<Task<T>> valueFactory;

    [NonSerialized]
    private object threadSafeObj;

    public AsyncLazy()
        : this(LazyThreadSafetyMode.ExecutionAndPublication)
    {
    }
    public AsyncLazy(Func<Task<T>> valueFactory)
                : this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication)
    {
    }

    public AsyncLazy(bool isThreadSafe) :
                this(isThreadSafe ?
                     LazyThreadSafetyMode.ExecutionAndPublication :
                     LazyThreadSafetyMode.None)
    {
    }

    public AsyncLazy(LazyThreadSafetyMode mode)
    {
        threadSafeObj = GetObjectFromMode(mode);
    }

    public AsyncLazy(Func<Task<T>> valueFactory, bool isThreadSafe)
                : this(valueFactory, isThreadSafe ? LazyThreadSafetyMode.ExecutionAndPublication : LazyThreadSafetyMode.None)
    {
    }

    public AsyncLazy(Func<Task<T>> valueFactory, LazyThreadSafetyMode mode)
    {
        if (valueFactory == null)
            throw new ArgumentNullException("valueFactory");

        threadSafeObj = GetObjectFromMode(mode);
        this.valueFactory = valueFactory;
    }

    private static object GetObjectFromMode(LazyThreadSafetyMode mode)
    {
        if (mode == LazyThreadSafetyMode.ExecutionAndPublication)
            return new object();
        if (mode == LazyThreadSafetyMode.PublicationOnly)
            return LazyHelpers.PUBLICATION_ONLY_SENTINEL;
        if (mode != LazyThreadSafetyMode.None)
            throw new ArgumentOutOfRangeException("mode");

        return null; // None mode
    }

    public override string ToString()
    {
        return IsValueCreated ? ((Boxed) boxed).value.ToString() : "NoValue";
    }

    internal LazyThreadSafetyMode Mode
    {
        get
        {
            if (threadSafeObj == null) return LazyThreadSafetyMode.None;
            if (threadSafeObj == (object)LazyHelpers.PUBLICATION_ONLY_SENTINEL) return LazyThreadSafetyMode.PublicationOnly;
            return LazyThreadSafetyMode.ExecutionAndPublication;
        }
    }
    internal bool IsValueFaulted
    {
        get { return boxed is LazyInternalExceptionHolder; }
    }

    public bool IsValueCreated
    {
        get
        {
            return boxed != null && boxed is Boxed;
        }
    }

    public async Task<T> FetchValueAsync()
    {
        Boxed boxed = null;
        if (this.boxed != null)
        {
            // Do a quick check up front for the fast path.
            boxed = this.boxed as Boxed;
            if (boxed != null)
            {
                return boxed.value;
            }

            LazyInternalExceptionHolder exc = this.boxed as LazyInternalExceptionHolder;
            exc.m_edi.Throw();
        }

        return await LazyInitValue().ConfigureAwait(false);
    }

    /// <summary>
    /// local helper method to initialize the value 
    /// </summary>
    /// <returns>The inititialized T value</returns>
    private async Task<T> LazyInitValue()
    {
        Boxed boxed = null;
        LazyThreadSafetyMode mode = Mode;
        if (mode == LazyThreadSafetyMode.None)
        {
            boxed = await CreateValue().ConfigureAwait(false);
            this.boxed = boxed;
        }
        else if (mode == LazyThreadSafetyMode.PublicationOnly)
        {
            boxed = await CreateValue().ConfigureAwait(false);
            if (boxed == null ||
                Interlocked.CompareExchange(ref this.boxed, boxed, null) != null)
            {
                boxed = (Boxed)this.boxed;
            }
            else
            {
                valueFactory = alreadyInvokedSentinel;
            }
        }
        else
        {
            object threadSafeObject = Volatile.Read(ref threadSafeObj);
            bool lockTaken = false;
            try
            {
                if (threadSafeObject != (object)alreadyInvokedSentinel)
                    Monitor.Enter(threadSafeObject, ref lockTaken);
                else
                    Contract.Assert(this.boxed != null);

                if (this.boxed == null)
                {
                    boxed = await CreateValue().ConfigureAwait(false);
                    this.boxed = boxed;
                    Volatile.Write(ref threadSafeObj, alreadyInvokedSentinel);
                }
                else
                {
                    boxed = this.boxed as Boxed;
                    if (boxed == null) // it is not Boxed, so it is a LazyInternalExceptionHolder
                    {
                        LazyInternalExceptionHolder exHolder = this.boxed as LazyInternalExceptionHolder;
                        Contract.Assert(exHolder != null);
                        exHolder.m_edi.Throw();
                    }
                }
            }
            finally
            {
                if (lockTaken)
                    Monitor.Exit(threadSafeObject);
            }
        }
        Contract.Assert(boxed != null);
        return boxed.value;
    }

    /// <summary>Creates an instance of T using valueFactory in case its not null or use reflection to create a new T()</summary>
    /// <returns>An instance of Boxed.</returns>
    private async Task<Boxed> CreateValue()
    {
        Boxed localBoxed = null;
        LazyThreadSafetyMode mode = Mode;
        if (valueFactory != null)
        {
            try
            {
                // check for recursion
                if (mode != LazyThreadSafetyMode.PublicationOnly && valueFactory == alreadyInvokedSentinel)
                    throw new InvalidOperationException("Recursive call to Value property");

                Func<Task<T>> factory = valueFactory;
                if (mode != LazyThreadSafetyMode.PublicationOnly) // only detect recursion on None and ExecutionAndPublication modes
                {
                    valueFactory = alreadyInvokedSentinel;
                }
                else if (factory == alreadyInvokedSentinel)
                {
                    // Another thread ----d with us and beat us to successfully invoke the factory.
                    return null;
                }
                localBoxed = new Boxed(await factory().ConfigureAwait(false));
            }
            catch (Exception ex)
            {
                if (mode != LazyThreadSafetyMode.PublicationOnly) // don't cache the exception for PublicationOnly mode
                    boxed = new LazyInternalExceptionHolder(ex);
                throw;
            }
        }
        else
        {
            try
            {
                localBoxed = new Boxed((T)Activator.CreateInstance(typeof(T)));
            }
            catch (MissingMethodException)
            {
                Exception ex = new MissingMemberException("Missing parametersless constructor");
                if (mode != LazyThreadSafetyMode.PublicationOnly) // don't cache the exception for PublicationOnly mode
                    boxed = new LazyInternalExceptionHolder(ex);
                throw ex;
            }
        }
        return localBoxed;
    }
}

这是否符合您的要求?

行为介于 ExecutionAndPublicationPublicationOnly 之间。

当初始化程序正在运行时,所有对 Value 的调用都将被交给相同的任务(临时缓存但随后可能成功或失败);如果初始化成功,那么完成的任务将被永久缓存;如果初始化程序失败,那么下一次调用 Value 将创建一个全新的初始化任务,并且该过程将重新开始!

public sealed class TooLazy<T>
{
    private readonly object _lock = new object();
    private readonly Func<Task<T>> _factory;
    private Task<T> _cached;

    public TooLazy(Func<Task<T>> factory)
    {
        if (factory == null) throw new ArgumentNullException("factory");
        _factory = factory;
    }

    public Task<T> Value
    {
        get
        {
            lock (_lock)
            {
                if ((_cached == null) ||
                    (_cached.IsCompleted && (_cached.Status != TaskStatus.RanToCompletion)))
                {
                    _cached = Task.Run(_factory);
                }
                return _cached;
            }
        }
    }
}

我使用的版本基于 :

// 
public class LazyTask
{
    public static LazyTask<T> Create<T>(Func<Task<T>> factory)
    {
        return new LazyTask<T>(factory);
    }
}

/// <summary>
/// Implements a caching/provisioning model we can term LazyThreadSafetyMode.ExecutionAndPublicationWithoutFailureCaching
/// - Ensures only a single provisioning attempt in progress
/// - a successful result gets locked in
/// - a failed result triggers replacement by the first caller through the gate to observe the failed state
///</summary>
/// <remarks>
/// Inspired by Stephen Toub http://blogs.msdn.com/b/pfxteam/archive/2011/01/15/asynclazy-lt-t-gt.aspx
/// Implemented with sensible semantics by @LukeH via SO 
/// </remarks>
public class LazyTask<T>
{
    readonly object _lock = new object();
    readonly Func<Task<T>> _factory;
    Task<T> _cached;

    public LazyTask(Func<Task<T>> factory)
    {
        if (factory == null) throw new ArgumentNullException("factory");
        _factory = factory;
    }

    /// <summary>
    /// Allow await keyword to be applied directly as if it was a Task<T>. See Value for semantics.
    /// </summary>
    public TaskAwaiter<T> GetAwaiter()
    {
        return Value.GetAwaiter();
    }

    /// <summary>
    /// Trigger a load attempt. If there is an attempt in progress, take that. If preceding attempt failed, trigger a retry.
    /// </summary>
    public Task<T> Value
    {
        get
        {
            lock (_lock) 
                if (_cached == null || BuildHasCompletedButNotSucceeded())
                    _cached = _factory();
            return _cached;
        }
    }

    bool BuildHasCompletedButNotSucceeded()
    {
        return _cached.IsCompleted && _cached.Status != TaskStatus.RanToCompletion;
    }
}