c# .net-4.0 thread-safety

c# - System.Lazy<T> con diferente modo de seguridad de hilo



.net-4.0 thread-safety (5)

La clase System.Lazy<T> .NET 4.0 ofrece tres modos Thread-Safety a través de la enumeración LazyThreadSafetyMode , que LazyThreadSafetyMode como:

  • LazyThreadSafetyMode.None : no es seguro para subprocesos.
  • LazyThreadSafetyMode.ExecutionAndPublication : solo un subproceso concurrente intentará crear el valor subyacente. En la creación exitosa, todos los hilos en espera recibirán el mismo valor. Si se produce una excepción no controlada durante la creación, se volverá a generar en cada subproceso en espera, se almacenará en caché y se volverá a generar en cada intento posterior de acceder al valor subyacente.
  • LazyThreadSafetyMode.PublicationOnly : varios subprocesos concurrentes intentarán crear el valor subyacente, pero el primero en tener éxito determinará el valor pasado a todos los subprocesos. Si se produce una excepción no controlada durante la creación, no se almacenará en caché y los intentos simultáneos y posteriores de acceder al valor subyacente volverán a intentar la creación y pueden tener éxito.

Me gustaría tener un valor de inicialización diferida que siga reglas de seguridad de subprocesos ligeramente diferentes, a saber:

Solo un hilo concurrente intentará crear el valor subyacente. En la creación exitosa, todos los hilos en espera recibirán el mismo valor. Si se produce una excepción no controlada durante la creación, se volverá a generar en cada subproceso en espera, pero no se almacenará en caché y los intentos posteriores de acceder al valor subyacente volverán a intentar la creación y pueden tener éxito.

Por lo tanto, la diferencia con LazyThreadSafetyMode.ExecutionAndPublication es que si falla un "primer intento" en la creación, se puede volver a intentar más adelante.

¿Existe una clase existente (.NET 4.0) que ofrezca estas semánticas, o tendré que pasar la mía? Si paso el mío, ¿hay alguna forma inteligente de reutilizar el Lazy <T> existente dentro de la implementación para evitar el bloqueo / sincronización explícito?

NB Para un caso de uso, imagine que la "creación" es potencialmente costosa y propensa a errores intermitentes, que implica, por ejemplo, obtener una gran porción de datos de un servidor remoto. No me gustaría hacer múltiples intentos simultáneos para obtener los datos, ya que probablemente todos fallarán o todos tendrán éxito. Sin embargo, si fallan, me gustaría poder volver a intentarlo más adelante.


Solo un hilo concurrente intentará crear el valor subyacente. En la creación exitosa, todos los hilos en espera recibirán el mismo valor. Si se produce una excepción no controlada durante la creación, se volverá a generar en cada subproceso en espera, pero no se almacenará en caché y los intentos posteriores de acceder al valor subyacente volverán a intentar la creación y pueden tener éxito.

Como Lazy no lo admite, puedes intentar rodarlo tú solo:

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

ACTUALIZAR:

Para cumplir con su requisito sobre la misma excepción propagada a todos los hilos de lectura en espera:

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


Algo como esto podría ayudar:

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


Lazy no es compatible con esto. Este es un problema de diseño con Lazy porque la "memoria caché" de excepción significa que esa instancia de Lazy no proporcionará un valor real para siempre. Esto puede hacer que las aplicaciones caigan permanentemente debido a errores transitorios, como problemas de red. Por lo general, se requiere intervención humana.

Apuesto a que esta mina existe en bastantes aplicaciones .NET ...

Necesitas escribir tu propio vago para hacer esto. O abra un problema de CoreFx Github para esto.


Mi intento de una versión de la respuesta actualizada de Darin que no tiene la condición de carrera que señalé ... advertencia, no estoy completamente seguro de que finalmente esté completamente libre de condiciones de carrera.

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

Actualización: pensé que encontré una condición de carrera después de publicar esto. El comportamiento en realidad debería ser aceptable, siempre y cuando esté bien con un caso presumiblemente raro en el que un hilo arroja una excepción que observó de un lento Lazy<T> lento después de que otro hilo ya haya regresado de un rápido y exitoso Lazy<T> (futuro todas las solicitudes tendrán éxito).

  • waiters = 0
  • t1: viene en ejecuciones hasta justo antes del Interlocked.Decrement ( waiters = 1)
  • t2: entra y se ejecuta justo antes del Interlocked.Increment . Interlocked.Increment ( waiters = 1)
  • t1: hace su Interlocked.Decrement y se prepara para sobrescribir ( waiters = 0)
  • t2: se ejecuta justo antes del Interlocked.Decrement ( waiters = 1)
  • t1: sobrescribe lazy con uno nuevo ( lazy1 ) ( waiters = 1)
  • t3: entra y bloquea en lazy1 ( waiters = 2)
  • t2: hace su Interlocked.Decrement ( waiters = 1)
  • t3: obtiene y devuelve el valor de lazy1 (los waiters ahora son irrelevantes)
  • t2: vuelve a lanzar su excepción

No se me ocurre una secuencia de eventos que causará algo peor que "este hilo arrojó una excepción después de que otro hilo arrojó un resultado exitoso".

Actualización 2: declarado lazy como volatile para garantizar que todos los lectores vean la sobrescritura guardada de inmediato. Algunas personas (incluido yo mismo) ven volatile e inmediatamente piensan "bueno, eso probablemente se está usando incorrectamente", y por lo general tienen razón. He aquí por qué lo usé aquí: en la secuencia de eventos del ejemplo anterior, t3 aún podría leer el lazy antiguo en lugar de lazy1 si se lazy1 justo antes de la lectura de lazy.Value el momento en que t1 modificó el lazy para contener el lazy1 . volatile protege contra eso para que el próximo intento pueda comenzar de inmediato.

También me recordé a mí mismo por qué tenía esta cosa en la parte posterior de mi cabeza diciendo "¡la programación concurrente de bloqueo bajo es difícil, solo use una declaración de lock C #!" todo el tiempo estuve escribiendo la respuesta original.

Actualización 3: acabo de cambiar un texto en la Actualización 2 que señala la circunstancia real que hace que la volatile sea ​​necesaria: las operaciones Interlocked que se usan aquí aparentemente se implementan completamente en las arquitecturas importantes de la CPU de hoy y no en la mitad, como originalmente solo había asumido, tan volatile protege una sección mucho más estrecha de lo que había pensado originalmente.


Parcialmente inspirado por la respuesta de Darin , pero tratando de hacer que esta "cola de hilos en espera que se infligen con la excepción" y las funciones "intentar de nuevo" funcionen:

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

Sin embargo, estoy un poco preocupado: ¿alguien puede ver carreras obvias aquí donde fracasará de una manera no obvia?