System.Lazy avec différents modes de sécurité des threads

La classe System.Lazy de .NET 4.0 offre trois modes Thread-Safety via l’énumération LazyThreadSafetyMode , que je résumerai comme suit:

  • LazyThreadSafetyMode.NoneNon thread-safe.
  • LazyThreadSafetyMode.ExecutionAndPublicationUn seul thread concurrent tentera de créer la valeur sous-jacente. En cas de création réussie, tous les threads en attente recevront la même valeur. Si une exception non gérée se produit lors de la création, elle sera rediffusée sur chaque thread en attente, mise en cache et rediffusée à chaque nouvelle tentative d’access à la valeur sous-jacente.
  • LazyThreadSafetyMode.PublicationOnlyPlusieurs threads simultanés tenteront de créer la valeur sous-jacente, mais le premier à réussir déterminera la valeur transmise à tous les threads. Si une exception non gérée se produit lors de la création, elle ne sera pas mise en cache et les tentatives simultanées d’access à la valeur sous-jacente tenteront à nouveau la création et pourraient réussir.

J’aimerais avoir une valeur paresseuse-initialisée qui suit des règles de sécurité du fil légèrement différentes, à savoir:

Un seul thread simultané tentera de créer la valeur sous-jacente. En cas de création réussie, tous les threads en attente recevront la même valeur. Si une exception non gérée se produit lors de la création, elle sera renvoyée sur chaque thread en attente, mais elle ne sera pas mise en cache et les tentatives suivantes pour accéder à la valeur sous-jacente tenteront à nouveau la création et pourraient réussir.

La différence avec LazyThreadSafetyMode.ExecutionAndPublication est donc que si un “premier lancement ” lors de la création échoue, il peut être tenté à nouveau plus tard.

Existe-t-il une classe (.NET 4.0) existante offrant cette sémantique ou devrai-je rouler la mienne? Si je lance le mien, y a-t-il un moyen intelligent de réutiliser le Lazy existant dans l’implémentation pour éviter le locking / synchronisation explicite?


NB Pour un cas d’utilisation, imaginez que la “création” soit potentiellement coûteuse et sujette aux erreurs intermittentes, impliquant par exemple l’obtention d’une grande quantité de données d’un serveur distant. Je ne voudrais pas faire plusieurs tentatives simultanées pour obtenir les données car elles échoueront probablement ou réussiront toutes. Cependant, s’ils échouent, j’aimerais pouvoir réessayer plus tard.

    Ma tentative d’une version de la réponse mise à jour de Darin qui ne présente pas la condition de course que j’ai signalée … avertissement, je ne suis pas complètement sûre que ce soit enfin totalement exempte de conditions de course.

     private static int waiters = 0; private static volatile Lazy lazy = new Lazy(GetValueFromSomewhere); public static object Value { get { Lazy 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(GetValueFromSomewhere); throw; } } } 

    Mise à jour: je pensais avoir trouvé une situation de concurrence critique après avoir posté ceci. Le comportement devrait en réalité être acceptable, tant que vous êtes d’accord avec un cas présumé rare dans lequel un thread lève une exception observée depuis un Lazy lent Lazy après qu’un autre thread est déjà rentré d’un succès Lazy (future les demandes vont toutes réussir).

    • waiters = 0
    • t1: arrive en cours d’exécution juste avant le Interlocked.Decrement ( waiters = 1)
    • t2: entre et court juste avant le Interlocked.Increment ( waiters = 1)
    • t1: fait son Interlocked.Decrement et se prépare à écraser ( waiters = 0)
    • t2: s’exécute juste avant le Interlocked.Decrement ( waiters = 1)
    • t1: remplace lazy un nouveau (appelez-le lazy1 ) ( waiters = 1)
    • t3: entre et bloque sur lazy1 ( waiters = 2)
    • t2: fait son Interlocked.Decrement ( waiters = 1)
    • t3: obtient et retourne la valeur de lazy1 (les waiters sont plus pertinents)
    • t2: renoue son exception

    Je ne peux pas proposer une séquence d’événements qui causera quelque chose de pire que “ce thread a lancé une exception après qu’un autre thread a donné un résultat positif”.

    Update2: déclaré lazy comme volatile pour s’assurer que l’écrasement protégé est immédiatement vu par tous les lecteurs. Certaines personnes (y compris moi-même) se sentent volatile et se disent tout de suite «bon, c’est probablement qu’elles sont mal utilisées», et elles ont généralement raison. Voici pourquoi je l’ai utilisé ici: dans la séquence d’événements de l’exemple ci-dessus, t3 pouvait toujours lire l’ancien lazy au lieu de lazy1 s’il était positionné juste avant la lecture de lazy.Value au moment où t1 modifiait lazy pour contenir lazy1 . volatile protège contre cela pour que la prochaine tentative puisse commencer immédiatement.

    Je me suis aussi rappelé pourquoi j’avais cette idée en tête: “La programmation concurrente à faible locking est difficile, utilisez simplement une instruction de lock C # !!!” Pendant tout ce temps, j’écrivais la réponse originale.

    Update3: vient de changer un texte dans Update2 soulignant la situation qui rend volatile – les opérations inter- Interlocked utilisées ici sont apparemment implémentées intégralement sur les architectures de processeur importantes d’aujourd’hui et non pas à demi-isolement comme je l’avais initialement sortingé supposé, si volatile protège une section beaucoup plus étroite que ce que je pensais à l’origine.

    Un seul thread simultané tentera de créer la valeur sous-jacente. En cas de création réussie, tous les threads en attente recevront la même valeur. Si une exception non gérée se produit lors de la création, elle sera renvoyée sur chaque thread en attente, mais elle ne sera pas mise en cache et les tentatives suivantes pour accéder à la valeur sous-jacente tenteront à nouveau la création et risquent de réussir.

    Puisque Lazy ne supporte pas cela, vous pouvez essayer de le faire vous-même:

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

    METTRE À JOUR:

    Afin de répondre à vos exigences concernant la même exception propagée à tous les threads de lecture en attente:

     private static Lazy lazy = new Lazy(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(GetTheValueFromSomewhere); // Re-throw the exception so that all blocked reader threads // will get this exact same exception thrown. throw; } } } 

    La paresseux ne supporte pas cela. Ceci est un problème de conception avec Lazy car une exception “mise en cache” signifie que cette instance paresseuse ne fournira pas une valeur réelle pour toujours. Cela peut interrompre les applications de manière permanente en raison d’erreurs transitoires telles que des problèmes de réseau. Une intervention humaine est alors généralement requirejse.

    Je parie que cette mine existe dans pas mal d’applications .NET …

    Vous devez écrire votre propre paresseux pour le faire. Ou bien, ouvrez un problème CoreFx Github pour cela.

    Partiellement inspiré par la réponse de Darin , mais en essayant d’obtenir cette “queue de threads qui sont infligés avec l’exception” et les fonctions “Réessayer” fonctionnent:

     private static Task _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(); 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; } } 

    Je suis cependant un peu inquiet – est-ce que quelqu’un peut voir ici des courses évidentes où il va échouer de manière non évidente?

    Quelque chose comme ça pourrait aider:

     using System; using System.Threading; namespace ADifferentLazy { ///  /// Basically the same as Lazy with LazyThreadSafetyMode of ExecutionAndPublication, BUT exceptions are not cached ///  public class LazyWithNoExceptionCaching { private Func valueFactory; private T value = default(T); private readonly object lockObject = new object(); private bool initialized = false; private static readonly Func ALREADY_INVOKED_SENTINEL = () => default(T); public LazyWithNoExceptionCaching(Func 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; } } } }