System.Lazy с различным режимом защиты от streamов

.NET System System.Lazy предлагает три режима Thread-Safety через enum LazyThreadSafetyMode , который я подытожу как:

  • LazyThreadSafetyMode.NoneНебезопасный stream.
  • LazyThreadSafetyMode.ExecutionAndPublicationтолько один параллельный stream попытается создать базовое значение. При успешном создании все ожидающие streamи получат одинаковое значение. Если во время создания возникает необработанное исключение, оно будет повторно загружено на каждый ожидающий stream, кэшироваться и повторно бросаться при каждой последующей попытке получить доступ к базовому значению.
  • LazyThreadSafetyMode.PublicationOnlyнесколько одновременных streamов попытаются создать базовое значение, но первое для успеха определит значение, переданное всем streamам. Если во время создания возникает необработанное исключение, оно не будет кэшироваться, а одновременные и последующие попытки получить доступ к базовому значению повторят попытку создания и могут преуспеть.

Я хотел бы иметь лениво-инициализированное значение, которое следует нескольким другим правилам безопасности streamов, а именно:

Только один параллельный stream попытается создать базовое значение. При успешном создании все ожидающие streamи получат одинаковое значение. Если во время создания возникает необработанное исключение, оно будет повторно выбрано для каждого ожидающего streamа, но оно не будет кэшироваться, и последующие попытки получить доступ к базовому значению повторят попытку создания и могут преуспеть.

Таким образом, ключ отличается от LazyThreadSafetyMode.ExecutionAndPublication в том, что если «первый переход» при создании не удается, его можно повторно попытаться позднее.

Есть ли существующий (.NET 4.0) class, который предлагает эту семантику, или мне придется сворачивать самостоятельно? Если я откажу свой собственный, есть ли способ повторного использования существующего Lazy в реализации, чтобы избежать явной блокировки / синхронизации?


NB. Для случая использования представьте, что «создание» потенциально дорого и подвержено прерывистой ошибке, включая, например, получение большого fragmentа данных с удаленного сервера. Я бы не хотел делать несколько параллельных попыток получить данные, так как они, скорее всего, потерпят неудачу или все удастся. Однако, если они терпят неудачу, я хотел бы повторить попытку позже.

    Моя попытка версии обновленного ответа Дарина, которая не имеет условия гонки, я указал … предупреждение, я не совсем уверен, что это, наконец, полностью свободно от условий гонки.

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

    Обновление: я думал, что нашел условие гонки после публикации этого. Поведение должно быть действительно приемлемым, если вы в порядке с предположительно редким случаем, когда какой-то stream генерирует исключение, которое он наблюдал из медленного Lazy после того как другой stream уже вернулся из успешного быстрого Lazy (будущий запросы будут успешными).

    • waiters = 0
    • t1: приходит в бегах до непосредственно перед Interlocked.Decrement waiters ( waiters = 1)
    • t2: входит и запускается непосредственно перед Interlocked.Increment ( waiters = 1)
    • t1: выполняет свою Interlocked.Decrement и Interlocked.Decrement переписывание ( waiters = 0)
    • t2: выполняется до момента Interlocked.Decrement waiters = 1)
    • t1: перезаписывает lazy новый (назовите его lazy1 ) ( waiters = 1)
    • t3: входит и блокируется на lazy1 ( waiters = 2)
    • t2: делает ли его Interlocked.Decrement waiters = 1)
    • t3: получает и возвращает значение от lazy1 ( waiters теперь неактуальны)
    • t2: повторяет свое исключение

    Я не могу придумать последовательность событий, которые приведут к чему-то худшему, чем «эта нить бросила исключение после того, как другой stream дал успешный результат».

    Update2: объявлено lazy как volatile чтобы гарантировать, что охраняемая перезапись будет видна всем читателям сразу. Некоторые люди (включая меня) видят volatile и сразу же думают «хорошо, что, вероятно, неправильно используются», и они обычно правы. Вот почему я использовал его здесь: в последовательности событий из приведенного выше примера t3 все равно мог читать старый lazy а не lazy1 если он был lazy1 как раз перед чтением lazy.Value момент, когда t1 изменил lazy чтобы содержать lazy1 . volatile защищает от этого, так что следующая попытка может начаться немедленно.

    Я также напомнил себе, почему у меня было это в затылке, говоря, что «одновременное программирование с низким уровнем блокировки сложно, просто используйте оператор C # lock !!!» все время, когда я писал оригинальный ответ.

    Update3: просто изменил какой-то текст в Update2, указав фактическое обстоятельство, которое делает необходимым volatile Операции Interlocked используемые здесь, по-видимому, реализованы в полнофункциональном режиме на важных архитектурах процессора сегодня, а не на половину, поскольку я изначально просто сортировал предположительно, так volatile защищает гораздо более узкий участок, чем я думал изначально.

    Только один параллельный stream попытается создать базовое значение. При успешном создании все ожидающие streamи получат одинаковое значение. Если во время создания возникает необработанное исключение, оно будет повторно выбрано для каждого ожидающего streamа, но оно не будет кэшироваться, и последующие попытки получить доступ к базовому значению повторят попытку создания и могут преуспеть.

    Поскольку Lazy не поддерживает это, вы можете попытаться свернуть его самостоятельно:

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

    ОБНОВИТЬ:

    Чтобы удовлетворить ваше требование об одном и том же исключении, распространяемом на все ожидающие темы чтения:

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

    Lazy не поддерживает это. Это проблема проектирования с Lazy, потому что исключение «кэширование» означает, что этот ленивый экземпляр не будет представлять реальную ценность навсегда. Это может привести к тому, что приложения будут работать постоянно из-за временных ошибок, таких как сетевые проблемы. Обычно требуется вмешательство человека.

    Уверен, что эта наземная мина существует в довольно многих .NET-приложениях …

    Вам нужно написать свой ленивый, чтобы сделать это. Или откройте для этого проблему CoreFx Github.

    Частично вдохновленный ответом Дарина , но пытаясь получить эту «очередь ожидающих streamов, которые были вызваны исключением» и «попробуйте еще раз», работает:

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

    Я немного обеспокоен тем, что – может ли кто-нибудь увидеть какие-либо очевидные гонки здесь, где он провалится неочевидным образом?

    Что-то вроде этого может помочь:

     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; } } } } 
    Давайте будем гением компьютера.