Кэширование результата из метода [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;
}
}