Кэширование результата из метода [n async] factory, если он не бросает

UPDATE: сильно изменился после того, как @usr указал, что я неправильно принял Lazy<T> режим безопасности по умолчанию по умолчанию был LazyThreadSafetyMode.PublicationOnly...

Я хочу лениво вычислить значение с помощью метода async Factory (т.е. возвращает Task<T>) и кэшировать его при успешном завершении. В случае исключения я хочу, чтобы это было доступно мне. Однако я не хочу жертвовать поведение кэширования исключений, которое Lazy<T> имеет в своем режиме по умолчанию (LazyThreadSafetyMode.ExecutionAndPublication)

Кэширование исключений. Когда вы используете методы Factory, исключения кэшируются. То есть, если метод Factory генерирует исключение, первый раз, когда поток пытается получить доступ к свойству Value объекта Lazy, одно и то же исключение бросается на каждую последующую попытку. Это гарантирует, что каждый вызов свойства Value дает тот же результат и избегает тонких ошибок, которые могут возникнуть, если разные потоки получают разные результаты. Lazy стоит за фактическое T, которое иначе было бы инициализировано в какой-то более ранней точке, обычно во время запуска. Сбой в этом более раннем пункте обычно является фатальным. Если есть потенциал для восстанавливаемого сбоя, мы рекомендуем построить логику повтора в процедуре инициализации (в данном случае, метод Factory), так же, как если бы вы не использовали ленивую инициализацию.

Стивен Тууб имеет класс AsyncLazy и запись, который выглядит правильно:

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> - если есть проблема, повторений не будет.

Я ищу эквивалент Task<T> Lazy<T>(Func<T>, LazyThreadSafetyMode.PublicationOnly), т.е. он должен вести себя так, как указано: -

Альтернатива блокировке В определенных ситуациях вы можете избежать накладных расходов на поведение блокировки по умолчанию Lazy. В редких случаях может возникнуть вероятность блокировок. В таких случаях вы можете использовать конструктор Lazy (LazyThreadSafetyMode) или Lazy (Func, LazyThreadSafetyMode) и указать LazyThreadSafetyMode.PublicationOnly. Это позволяет объекту Lazy создавать копию лениво инициализированного объекта для каждого из нескольких потоков, если потоки одновременно называют свойство Value. Объект Lazy гарантирует, что все потоки используют один и тот же экземпляр лениво инициализированного объекта и отбрасывают экземпляры, которые не используются. Таким образом, стоимость сокращения накладных расходов на блокирование заключается в том, что иногда ваша программа может создавать и отбрасывать дополнительные копии дорогостоящего объекта. В большинстве случаев это маловероятно. Эти примеры демонстрируют примеры конструкторов Lazy (LazyThreadSafetyMode) и Lazy (Func, LazyThreadSafetyMode).

ВАЖНО

Когда вы указываете PublicationOnly, исключения никогда не кэшируются, даже если вы укажете метод Factory.

Есть ли какой-либо FCL, Nito.AsyncEx или подобный конструкт, который может быть здесь очень хорош? В противном случае кто-нибудь может увидеть элегантный способ заблокировать бит "попытка в процессе" (я в порядке с каждым вызывающим, делающим свою попытку таким же образом, что и Lazy<T>(..., (LazyThreadSafetyMode.PublicationOnly)), и все же все еще есть, и управление кэшем инкапсулировано аккуратно?

Ответ 1

Значит ли это, что-то рядом с вашими требованиями?

Поведение падает где-то между ExecutionAndPublication и PublicationOnly.

В то время как инициализатор находится в полете, все вызовы на 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;
            }
        }
    }
}

Ответ 2

Отказ от ответственности: Это дикая попытка рефакторинга Lazy<T>. Это никоим образом не является производственным кодом.

Я взял на себя смелость взглянуть на исходный код Lazy<T> и немного изменить его для работы с Func<Task<T>>. Я переработал свойство Value, чтобы стать FetchValueAsync, поскольку мы не можем ждать внутри свойства. Вы можете заблокировать операцию async с помощью Task.Result, чтобы вы все еще могли использовать свойство 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;
    }
}

Ответ 3

В настоящее время я использую это:

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

Пока он работает в моем сценарии (у меня нет нескольких потоков запуска и т.д.), он вряд ли является элегантным и не обеспечивает потокобезопасную координацию либо

  • выполняется одна попытка a la LazyThreadSafetyMode.ExecutionAndPublication ИЛИ
  • стабильный результат после >= 1 успеха a la LazyThreadSafetyMode.PublicationOnly

Ответ 4

Версия, используемая на основе ответа @LukeH. Пожалуйста, поддержите это, а не это.

// http://stackoverflow.com/a/33872589/11635
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 http://stackoverflow.com/a/33942013/11635
/// </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;
    }
}