almacenando en caché el resultado de un método de fábrica si no arroja

ACTUALIZACIÓN: Muy revisada después de que @usr señaló que había asumido incorrectamente que el modo de seguridad de subprocesos por defecto de Lazy era LazyThreadSafetyMode.PublicationOnly

Quiero calcular de forma diferida un valor a través de un Método de fábrica async (es decir, devuelve Task ) y tenerlo en caché cuando tenga éxito. A excepción, quiero que esté disponible para mí. Sin embargo, no quiero caer presa del comportamiento de caché de excepción que tiene Lazy en su modo predeterminado ( LazyThreadSafetyMode.ExecutionAndPublication )

Caché de excepción: cuando utiliza métodos de fábrica, las excepciones se guardan en caché. Es decir, si el método de fábrica arroja una excepción la primera vez que un subproceso intenta acceder a la propiedad Value del objeto Lazy, se lanza la misma excepción en cada bash posterior. Esto garantiza que cada llamada a la propiedad Value produce el mismo resultado y evita los errores sutiles que pueden surgir si diferentes subprocesos obtienen resultados diferentes. The Lazy representa una T real que de otro modo se habría inicializado en algún punto anterior, generalmente durante el inicio. Una falla en ese punto anterior generalmente es fatal. Si existe la posibilidad de una falla recuperable, le recomendamos que cree la lógica de rebash en la rutina de inicialización (en este caso, el método de fábrica), tal como lo haría si no estuviera utilizando la inicialización diferida.

Stephen Toub tiene una clase AsyncLazy y una redacción que parece correcta:

 public class AsyncLazy : Lazy<Task> { public AsyncLazy(Func<Task> taskFactory) : base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap()) { } public TaskAwaiter GetAwaiter() { return Value.GetAwaiter(); } } 

sin embargo, ese es efectivamente el mismo comportamiento que un Lazy predeterminado: si hay un problema, no habrá rebashs.

Estoy buscando un equivalente compatible con Task de Lazy(Func, LazyThreadSafetyMode.PublicationOnly) , es decir, debería comportarse como se especifica: –

Alternativa al locking En ciertas situaciones, es posible que desee evitar la sobrecarga del comportamiento de locking predeterminado del objeto Lazy. En situaciones excepcionales, puede haber un potencial para estancamientos. En tales casos, puede usar el constructor Lazy (LazyThreadSafetyMode) o Lazy (Func, LazyThreadSafetyMode) y especificar LazyThreadSafetyMode.PublicationOnly. Esto permite que el objeto Lazy cree una copia del objeto inicializado de forma pausada en cada uno de varios hilos si los hilos llaman simultáneamente a la propiedad Value. El objeto Lazy garantiza que todos los subprocesos usen la misma instancia del objeto inicializado de forma diferida y descarta las instancias que no se usan. Por lo tanto, el costo de reducir el costo de locking es que su progtwig algunas veces puede crear y descartar copias adicionales de un objeto costoso. En la mayoría de los casos, esto es poco probable. Los ejemplos de los constructores Lazy (LazyThreadSafetyMode) y Lazy (Func, LazyThreadSafetyMode) demuestran este comportamiento.

IMPORTANTE

Cuando especifica PublicationOnly, las excepciones nunca se almacenan en caché, incluso si especifica un método de fábrica.

¿Hay alguna construcción FCL, Nito.AsyncEx o similar que encaje bien aquí? Si esto falla, ¿alguien puede ver una forma elegante de cerrar el bit de “bash en progreso” (estoy de acuerdo con que cada persona que llama haga su propio bash de la misma manera que un Lazy( …, (LazyThreadSafetyMode.PublicationOnly) hace) y aún así tener eso y la gestión de la caché encapsulado prolijamente?

¿Esto se acerca a tus necesidades?

El comportamiento se ubica en algún lugar entre ExecutionAndPublication y PublicationOnly .

Mientras el inicializador está en vuelo, todas las llamadas a Value recibirán la misma tarea (que se almacenará en caché temporalmente pero que posteriormente podría tener éxito o fallar); si el inicializador tiene éxito, entonces esa tarea completa se almacena en caché permanentemente; si el inicializador falla, la próxima llamada a Value creará una tarea de inicialización completamente nueva y ¡el proceso comenzará de nuevo!

 public sealed class TooLazy { private readonly object _lock = new object(); private readonly Func> _factory; private Task _cached; public TooLazy(Func> factory) { if (factory == null) throw new ArgumentNullException("factory"); _factory = factory; } public Task Value { get { lock (_lock) { if ((_cached == null) || (_cached.IsCompleted && (_cached.Status != TaskStatus.RanToCompletion))) { _cached = Task.Run(_factory); } return _cached; } } } } 

Descargo de responsabilidad: Este es un bash salvaje de refactorizar Lazy . De ninguna manera es un código de grado de producción.

Me tomé la libertad de mirar el código fuente Lazy y modificarlo un poco para que funcione con Func> . Refactoricé la propiedad Value para convertirme en un método FetchValueAsync , ya que no podemos esperar dentro de una propiedad. Puede bloquear la operación async con Task.Result para que pueda seguir utilizando la propiedad Value , no quería hacer eso porque puede ocasionar problemas. Entonces es un poco más engorroso, pero aún funciona. Este código no está completamente probado:

 public class AsyncLazy { 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> alreadyInvokedSentinel = delegate { Contract.Assert(false, "alreadyInvokedSentinel should never be invoked."); return default(Task); }; private object boxed; [NonSerialized] private Func> valueFactory; [NonSerialized] private object threadSafeObj; public AsyncLazy() : this(LazyThreadSafetyMode.ExecutionAndPublication) { } public AsyncLazy(Func> 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> valueFactory, bool isThreadSafe) : this(valueFactory, isThreadSafe ? LazyThreadSafetyMode.ExecutionAndPublication : LazyThreadSafetyMode.None) { } public AsyncLazy(Func> 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 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); } ///  /// local helper method to initialize the value ///  /// The inititialized T value private async Task 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; } /// Creates an instance of T using valueFactory in case its not null or use reflection to create a new T() /// An instance of Boxed. private async Task 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> 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; } } 

Por ahora, estoy usando esto:

 public class CachedAsync { readonly Func> _taskFactory; T _value; public CachedAsync(Func> taskFactory) { _taskFactory = taskFactory; } public TaskAwaiter GetAwaiter() { return Fetch().GetAwaiter(); } async Task Fetch() { if (_value == null) _value = await _taskFactory(); return _value; } } 

Si bien funciona en mi escenario (no tengo múltiples hilos de activación, etc.), no es muy elegante y no proporciona una coordinación segura de subprocesos

  • un solo bash en progreso a la LazyThreadSafetyMode.ExecutionAndPublication OR
  • un resultado estable después de> = 1 éxito a la LazyThreadSafetyMode.PublicationOnly

Versión como la estoy usando en base a la respuesta de @ LukeH. Por favor vota eso, no esto.

 // http://stackoverflow.com/a/33872589/11635 public class LazyTask { public static LazyTask Create(Func> factory) { return new LazyTask(factory); } } ///  /// 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 /// ///  /// 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 ///  public class LazyTask { readonly object _lock = new object(); readonly Func> _factory; Task _cached; public LazyTask(Func> factory) { if (factory == null) throw new ArgumentNullException("factory"); _factory = factory; } ///  /// Allow await keyword to be applied directly as if it was a Task. See Value for semantics. ///  public TaskAwaiter GetAwaiter() { return Value.GetAwaiter(); } ///  /// Trigger a load attempt. If there is an attempt in progress, take that. If preceding attempt failed, trigger a retry. ///  public Task Value { get { lock (_lock) if (_cached == null || BuildHasCompletedButNotSucceeded()) _cached = _factory(); return _cached; } } bool BuildHasCompletedButNotSucceeded() { return _cached.IsCompleted && _cached.Status != TaskStatus.RanToCompletion; } }