Re[2]: Interlocked
От: RushDevion Россия  
Дата: 28.12.18 10:07
Оценка: 6 (2) +1
Здравствуйте, RAza, Вы писали:

S>>А чем, кстати, Interlocked не подошел?

RD>>3. И да, можно переписать на Interlocked.CompareExchange и обойтись без lock.
RA>Никогда не использовал это на практике. Вас не затруднит показать реализацию? И в двух словах объяснить в чем плюсы по сравнению с DCL?

Для начала, зачем все это надо.
1. Иногда код алгоритма на Interlocked-инструкциях получается чище, чем с применением примитивов синхронизации (lock, производные WaitHandle и т.п.)
2. Мы экономим объект (тот, на котором делается lock).
3. Interlocked-инструкции быстрее (ns против ms у других примитивов). Поэтому в высококонкурентных lock-free алгоритмах используют именно их.

И сразу оговорюсь, что рядовому разработчику корпоративного софта (ну типа меня ) исчезающе редко (т.е. практически никогда)
приходится писать код, где разница в производительности между Interlocked/не-Interlocked ощутимо влияет на performance.

Теперь по реализации. Покажу на примере getter'a для Value
public class Lazy<T> where T : class
{
    private readonly Func<T> m_Factory;
    private int m_Initializing = 0;
    private T m_Value;

    public Lazy(Func<T> factory) { m_Factory = factory; }

    public T Value
    {
        get
        {
            var curVal = Volatile.Read(ref m_Value);
            if (curVal != default(T)) return curVal;
        
            // Самый простой и красивый вариант. 
            // Когда я предлагал Interlocked, то думал именно о нем.
            // Атомарно проверяем, что m_Value == null, если да - инициализируем.
            // К сожалениею, он не обеспечивает требование exact-once инициализации.
            Interlocked.CompareExchange(ref m_Value, m_Factory(), default(T));
            return Volatile.Read(ref m_Value);


            // Вариант с exact-once инициализацией (без обработки ошибок)
            if (Interlocked.CompareExchange(ref m_Initializing, 1, 0) == 0)
            {
                // Захватили право на инициализацию, инициируем
                var val = m_Factory();
                Volatile.Write(ref m_Value, val);
                return val;
            }

            // Если мы попали сюда, то право на инициализацию захватил кто-то другой, будем ждать, пока он проинициализирует
            while (true)
            {
                var val = Volatile.Read(ref m_Value);
                if (val != default(T)) return val;

                // Это так называемый Sleep-wait, 
                // В lock-free часто применяют SpinWait/Thread.Yield либо их комбинации
                Thread.Sleep(TimeSpan.FromMilliseconds(5));
            }
        }
    }
}


А если добавить обработку ошибок инициализации, то код будет еще сложнее. При этом профит от lock-free выглядит более чем сомнительным.
Так что рассматривай это скорее как академический пример.
Re: Собственная потокобезопасная реализация Lazy<T>
От: RushDevion Россия  
Дата: 26.12.18 21:59
Оценка: +2
RA>1. действительно ли данный тип является потокобезопасным?
Да. Это стандартный double check lock.
RA>2. может ли быть race condition который я пропустил?
Я не увидел.

Не знаю, зачем тебе это, но если это production-код, то:
1. Нет проверки на null для getAction
2. Volatile.Read/Write из-под lock не имеют смысла, т.к. lock уже ставит все memory barriers. Можно просто читать/писать в переменную.
3. И да, можно переписать на Interlocked.CompareExchange и обойтись без lock.
Re[6]: Interlocked
От: GlebZ Россия  
Дата: 29.12.18 13:22
Оценка: 6 (1)
Здравствуйте, Sharov, Вы писали:

S>За чем что-то генерировать, когда есть конкретне инсрукции процессора? Это для msil, где про енто можно почитать? Иначе это будет похоже на транзакционную память(или вроде ентого). Т.е. так

S>можно и FFT атомарно делать.

S>Еще раз, можно ссылку на генерацию lock и т.д. ибо я себе это нескольок иначе представлял. Вомзожно я неправ.

Первая ссылка из гугла
http://www.club155.ru/x86cmd/LOCK
Re[4]: Interlocked
От: RushDevion Россия  
Дата: 29.12.18 10:32
Оценка: 3 (1)
RA>Большое спасибо за пример. Основную идею я понял. В посте ниже справедливо указали, на то, что моя реализация не отвечает заявленным критериям, а именно при возникновении ошибки при получении значения ожидающие потоки не получат тот же объект исключения. Сейчас набросал небольшой тест кейс и думаю как модифицировать мою реализацию. Пока безуспешно.
Как идея: можно возвращать Task. Тогда проброс ошибок будет автоматическим.
public class Lazy<T>
{
    private Task<T> m_Value;
    private readonly Func<Task<T>> m_Factory;

    public Lazy(Func<Task<T>> factory) { m_Factory = factory; }

    public Task<T> GetValueAsync()
    {
        // Запускаем инициализацию
        Interlocked.CompareExchange(ref m_Value, m_Factory(), null);
        var task = Volatile.Read(ref m_Value);
        
        // Ошибка инициализации? Сбросим текущую таску в null, чтобы перезапуститься при след. обращении
        task.ContinueWith(_ =>
                Interlocked.CompareExchange(ref m_Value, null, task),
            TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);

        return task;
    }
}



RA>На сколько понимаю это связано из-за ограничения методов Volatile.* Это можно как то обойти?

Да, Volatile работает с очень ограниченным набором типов.
Ну можно использовать вложенный класс, типа такого:
public class Lazy<T>
{
    private ValueHolder m_ValueHolder;
    private Func<T> m_Factory;

    private  class ValueHolder
    {
        public T Value;
    }

    private T Value
    {
        get
        {
            Interlocked.CompareExchange(ref m_ValueHolder, new ValueHolder {Value = m_Factory()}, null);
            var holder = Volatile.Read(ref m_ValueHolder);
            return holder.Value;
        }
    }
}
Re[7]: Interlocked
От: Sharov Россия  
Дата: 29.12.18 11:32
Оценка: +1
Здравствуйте, RushDevion, Вы писали:

RD>Здравствуйте, Sharov, Вы писали:

S>>Здравствуйте, RushDevion, Вы писали:
RD>>> public Task<T> GetValueAsync()
RD>>> {

RD>>> // Ошибка инициализации? Сбросим текущую таску в null, чтобы перезапуститься при след. обращении

RD>>> task.ContinueWith(_ =>
RD>>> Interlocked.CompareExchange(ref m_Value, null, task),
RD>>> TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);

RD>>> return task;

RD>>> }

S>>А тут rc не будет случаем? Может continuation прицепить к родителю (task)?

RD>Так оно и цепляется к task
RD>Я возможностей для rc не вижу.

Пользователь может воспользовать знач. task, до того как оно сброситься в null.

Уточню, что прицепив его AttachToParent, task не будет считаться законченным, пока не закончаться его потомки. Как-то так...(могу ошибаться)
Кодом людям нужно помогать!
Отредактировано 29.12.2018 11:43 Sharov . Предыдущая версия .
Re[7]: Interlocked
От: Sharov Россия  
Дата: 29.12.18 13:29
Оценка: +1
Здравствуйте, GlebZ, Вы писали:

GZ>Первая ссылка из гугла

GZ>http://www.club155.ru/x86cmd/LOCK

Был неправ, т.е. не знал детали. Век живи -- век учись.
Кодом людям нужно помогать!
Re[6]: Interlocked
От: Sharov Россия  
Дата: 04.01.19 17:36
Оценка: +1
Здравствуйте, RAza, Вы писали:


RA> catch (Exception ex)

RA> {
RA> Interlocked.Exchange(ref isExist, 0);

if (waiting > 0)
{
onError.Set();
error = ex;
]
}

RA> throw;

RA> }


Может местами поменять публикацию ошибку и нотификацию?
Кодом людям нужно помогать!
Собственная потокобезопасная реализация Lazy<T>
От: RAza  
Дата: 26.12.18 12:11
Оценка:
Приветствую.

Хочу спроектировать тип аналогичный типу Lazy<T> или другими словами предоставляющий функционал аналогичный следующему сниппету:

private T? value = default;
 
public T Value
{
    get
    {
        if (value.HasValue)
            return value.Value;

        return (value = ...).Value;
    }
    set
    {
        this.value = value;
    }
}


Цели, которые я преследую:

    1. Поведение данного типа при доступе к экземпляру из нескольких конкурентных потоков аналогично поведению типа Lazy<T> при использовании режима LazyThreadSafetyMode.ExecutionAndPublication за исключением того, что исключения полученные при использовании фабричного метода НЕ будут кэшированы. Таким образом только один конкурентный поток попытается создать экземпляр указанного типа; при успешном создании все ожидающие потоки получат одинаковое значение; если во время создания возникает необработанное исключение, оно будет повторно создано для каждого ожидающего потока, но оно не будет кэшироваться, и последующие попытки получить доступ к значению повторят попытку создания и могут быть успешными.
    2. Тип является потокобезопасным

public class MyLazy<T>
{
    private readonly Func<T> getAction = null;

    private readonly object obj = new object();

    private readonly bool canBeReseted = false;

    private bool isExist = false;

    private T value = default;

    public bool HasValue => isExist;

    public T Value
    {
        get
        {
            if (Volatile.Read(ref isExist))
                return value;

            lock (obj)
            {
                if (Volatile.Read(ref isExist))
                    return value;

                value = getAction();

                Volatile.Write(ref isExist, true);
            }

            return value;
        }
        set
        {
            lock (obj)
            {
                this.value = value;

                Volatile.Write(ref isExist, true);
            }
        }
    }

    public MyLazy(Func<T> getAction, bool canBeReseted)
            : this(getAction)
        => this.canBeReseted = canBeReseted;

    public MyLazy(Func<T> getAction)
        => this.getAction = getAction;

    public void Reset()
    {
        if (canBeReseted == false)
            return;

        lock (obj)
        {
            Volatile.Write(ref this.isExist, false);
        }
    }
}


Вопросы:

    1. действительно ли данный тип является потокобезопасным?
    2. может ли быть race condition который я пропустил?
Отредактировано 26.12.2018 12:37 RAza . Предыдущая версия .
Re: Собственная потокобезопасная реализация Lazy<T>
От: Sharov Россия  
Дата: 26.12.18 12:51
Оценка:
Здравствуйте, RAza, Вы писали:

1)Для инициализации см. сюда -- https://en.wikipedia.org/wiki/Double-checked_locking.
2)Зачем Volatile.*, когда переменную можно объявить с модификатором volatile?
Кодом людям нужно помогать!
Re[2]: Собственная потокобезопасная реализация Lazy<T>
От: RAza  
Дата: 26.12.18 13:18
Оценка:
Здравствуйте, Sharov, Вы писали:

S>1)Для инициализации см. сюда -- https://en.wikipedia.org/wiki/Double-checked_locking.

На сколько я понимаю у меня инициализация как раз находится внутри конструкции DCL.

S>2)Зачем Volatile.*, когда переменную можно объявить с модификатором volatile?

Набор личных предубеждений. Вроде бы это ни на что не должно повлиять в данном случае?
Отредактировано 26.12.2018 13:19 RAza . Предыдущая версия .
Re[3]: Собственная потокобезопасная реализация Lazy<T>
От: Sharov Россия  
Дата: 26.12.18 14:08
Оценка:
Здравствуйте, RAza, Вы писали:

S>>2)Зачем Volatile.*, когда переменную можно объявить с модификатором volatile?

RA>Набор личных предубеждений. Вроде бы это ни на что не должно повлиять в данном случае?

А чем, кстати, Interlocked не подошел?
Кодом людям нужно помогать!
Re: Собственная потокобезопасная реализация Lazy<T>
От: Vladek Россия Github
Дата: 26.12.18 23:00
Оценка:
Здравствуйте, RAza, Вы писали:

RA>Приветствую.


RA>Хочу спроектировать тип аналогичный типу Lazy<T> или другими словами предоставляющий функционал аналогичный следующему сниппету:


Слабо понял, что нужно, но советую сначала почитать это https://stuartlang.uk/miscellaneous-csharp-async-tips/

Там упоминаются две либы, возможно ничего изобретать не придётся.

https://github.com/Microsoft/vs-threading
https://github.com/StephenCleary/AsyncEx
Re: Interlocked
От: RAza  
Дата: 27.12.18 12:23
Оценка:
S>А чем, кстати, Interlocked не подошел?
RD>3. И да, можно переписать на Interlocked.CompareExchange и обойтись без lock.

Никогда не использовал это на практике. Вас не затруднит показать реализацию? И в двух словах объяснить в чем плюсы по сравнению с DCL?
Re[2]: Собственная потокобезопасная реализация Lazy<T>
От: RAza  
Дата: 27.12.18 12:31
Оценка:
Здравствуйте, RushDevion, Вы писали:

RD>Не знаю, зачем тебе это, но...

Несколько потоков, работают с этим поставщиком данных. Логика у потоков различная (условно некоторые только читают, другие в ряде случаев могут обновть значение или "сбросить"). Но всем для работы нужно данное значение. Допустим, это значение можно получить через сеть, поэтому я хочу, чтобы только один из потоков создал запрос на его получение. Запрос может закончиться с исключением, и это означает, что все потоки должны попытаться получить его позже. Когда значение получено, некоторые потоки могут его изменить, а все остальные должны использовать обновленное на следующих итерациях (обращениях к поставщику).

RD>1. Нет проверки на null для getAction

Я использую Fody NullGuard. В примере убрал все "лишние".

RD>2. Volatile.Read/Write из-под lock не имеют смысла, т.к. lock уже ставит все memory barriers. Можно просто читать/писать в переменную.

Это может оказать влияние на производительность?
Re[2]: Собственная потокобезопасная реализация Lazy<T>
От: RAza  
Дата: 27.12.18 12:34
Оценка:
Здравствуйте, Vladek, Вы писали:

V>Слабо понял, что нужно, но советую сначала почитать это https://stuartlang.uk/miscellaneous-csharp-async-tips/

V>Там упоминаются две либы, возможно ничего изобретать не придётся.
V>https://github.com/Microsoft/vs-threading
V>https://github.com/StephenCleary/AsyncEx

Юз-кейс описал в ветке выше. За статью спасибо! С AsyncEx знаком. Во второй типа с необходимым мне поведением с виду нет.
Re[2]: Interlocked
От: Sharov Россия  
Дата: 27.12.18 13:35
Оценка:
Здравствуйте, RAza, Вы писали:


S>>А чем, кстати, Interlocked не подошел?

RD>>3. И да, можно переписать на Interlocked.CompareExchange и обойтись без lock.

RA>Никогда не использовал это на практике. Вас не затруднит показать реализацию? И в двух словах объяснить в чем плюсы по сравнению с DCL?


Я хотело вместо Volatile.Read посоветовать Interlocked.Read, но он только для int64. Смотрите сюда. C dcl все нормально, Volatile.* я бы убрал.
Кодом людям нужно помогать!
Re: Собственная потокобезопасная реализация Lazy<T>
От: pugv Россия  
Дата: 27.12.18 15:09
Оценка:
Здравствуйте, RAza, Вы писали:

RA> если во время создания возникает необработанное исключение, оно будет повторно создано для каждого ожидающего потока


Такого поведения тут нет. Для каждого будет вызываться getAction.
Re[2]: Собственная потокобезопасная реализация Lazy<T>
От: Sharov Россия  
Дата: 27.12.18 15:50
Оценка:
Здравствуйте, pugv, Вы писали:

P>Здравствуйте, RAza, Вы писали:


RA>> если во время создания возникает необработанное исключение, оно будет повторно создано для каждого ожидающего потока


P>Такого поведения тут нет. Для каждого будет вызываться getAction.


Если getAction отработал без ошибок, то он будет вызван только 1 раз.
Кодом людям нужно помогать!
Re[3]: Собственная потокобезопасная реализация Lazy<T>
От: pugv Россия  
Дата: 27.12.18 15:53
Оценка:
Здравствуйте, Sharov, Вы писали:

RA>>> если во время создания возникает необработанное исключение, оно будет повторно создано для каждого ожидающего потока


S>Если getAction отработал без ошибок, то он будет вызван только 1 раз.


Речь о необработанных исключениях и ожидающих в это время на локе потоках.
Re[3]: Interlocked
От: RAza  
Дата: 29.12.18 09:37
Оценка:
Здравствуйте, RushDevion, Вы писали:

RD>Теперь по реализации. Покажу на примере getter'a для Value

RD>А если добавить обработку ошибок инициализации, то код будет еще сложнее. При этом профит от lock-free выглядит более чем сомнительным.
RD>Так что рассматривай это скорее как академический пример.

Большое спасибо за пример. Основную идею я понял. В посте ниже справедливо указали, на то, что моя реализация не отвечает заявленным критериям, а именно при возникновении ошибки при получении значения ожидающие потоки не получат тот же объект исключения. Сейчас набросал небольшой тест кейс и думаю как модифицировать мою реализацию. Пока безуспешно.

На первый взгляд в вашей реализации добавить код для обработки ошибок инициализации значительно проще. Достаточно в цикле бросить исключение для всех ожидающих потоков и изменить механизм Sleep-wait.
Но меня не устраивает ограничение:

RD>
RD>public class Lazy<T> where T : class
RD>...
RD>


На сколько понимаю это связано из-за ограничения методов Volatile.* Это можно как то обойти?
Re[5]: Interlocked
От: Sharov Россия  
Дата: 29.12.18 10:46
Оценка:
Здравствуйте, RushDevion, Вы писали:

RD> public Task<T> GetValueAsync()

RD> {

RD> // Ошибка инициализации? Сбросим текущую таску в null, чтобы перезапуститься при след. обращении

RD> task.ContinueWith(_ =>
RD> Interlocked.CompareExchange(ref m_Value, null, task),
RD> TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);

RD> return task;

RD> }

А тут rc не будет случаем? Может continuation прицепить к родителю (task)?
Кодом людям нужно помогать!
Re[6]: Interlocked
От: RushDevion Россия  
Дата: 29.12.18 11:19
Оценка:
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, RushDevion, Вы писали:
RD>> public Task<T> GetValueAsync()
RD>> {

RD>> // Ошибка инициализации? Сбросим текущую таску в null, чтобы перезапуститься при след. обращении

RD>> task.ContinueWith(_ =>
RD>> Interlocked.CompareExchange(ref m_Value, null, task),
RD>> TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);

RD>> return task;

RD>> }

S>А тут rc не будет случаем? Может continuation прицепить к родителю (task)?

Так оно и цепляется к task
Я возможностей для rc не вижу.

Но тут может быть множественная инициализация.
Если два и более потоков придут одновременно, то каждый из них запустит инициализацию.
Но востребованной будет только одна. Невостребованные либо тихо умрут, либо (худший случай) зафейлятся и улетят в Unobseved Task Exception handler.
Я же сказал, что это идея, а не production code
Re[2]: Interlocked
От: GlebZ Россия  
Дата: 29.12.18 11:59
Оценка:
Здравствуйте, RAza, Вы писали:


S>>А чем, кстати, Interlocked не подошел?

RD>>3. И да, можно переписать на Interlocked.CompareExchange и обойтись без lock.

RA>Никогда не использовал это на практике. Вас не затруднит показать реализацию? И в двух словах объяснить в чем плюсы по сравнению с DCL?

Для справки:
volatile — указание компилятору что к этой переменной может быть обращение с другого потока, потому значение должно сохраняться в ОЗУ и компилятор не должен переупорядочивать команды
interlocked — это указание компилятору о защищенной команде, что выливается на уровне ассемблера в генерацию префикса lock для текущей команды, например при инкрементации значения с возвратом lock xadd xxx,xxx Чрезвычайно быстрая хрень.
lock — блокирование по примерному алгоритму, сначало проверяется через interlocked команды, что делается очень быстро. Если заблокировано, то блокируем через объект ядра (что не так быстро по сравнению с interlocked командами).

Но это все для задротов.
Re[8]: Interlocked
От: RushDevion Россия  
Дата: 29.12.18 12:15
Оценка:
S>>>А тут rc не будет случаем? Может continuation прицепить к родителю (task)?
RD>>Так оно и цепляется к task
RD>>Я возможностей для rc не вижу.

S>Пользователь может воспользовать знач. task, до того как оно сброситься в null.

Так это нормально. Последовательность примерно такая:
1. Пришел поток1. Создал таску инициализации. Ждет ее результата.
2. Пришел поток2. Ему отдали готовую таску, созданную потоком1.
3. Таска зафейлилась. Каждый из потоков получил свой Exception. На первом потоке запустился continuation (ExecuteSynchronously), который должен сбросить task в null
4. В этот момент пришел поток3.
Для него возможны два сценария:
4.1. Поток1 еще не успел сбросить task в null. В этом случае поток3 получит тот же самый task, что потоки1/2 и сразу зафейлится (ну не повезло, че).
4.2. Поток1 успел сбросить task. В этом случае поток3 запустит таску инициализации по-новой.

S>Уточню, что прицепив его AttachToParent, task не будет считаться законченным, пока не закончаться его потомки. Как-то так...(могу ошибаться)

Да, пожалуй, есть смысл добавить и AttachedToParent.
Если потоки получают Value в цикле это исключит вероятность 4.1 для потока2 при повторной попытке получения Value.
Re[3]: Interlocked
От: Sharov Россия  
Дата: 29.12.18 12:33
Оценка:
Здравствуйте, GlebZ, Вы писали:

GZ>interlocked — это указание компилятору о защищенной команде, что выливается на уровне ассемблера в генерацию префикса lock для текущей команды, например при инкрементации значения с возвратом lock xadd xxx,xxx Чрезвычайно быстрая хрень.


Что за дичь написана? Это атомарные инструкции процессора (буквально одна инструкция процессора), а не генерация префиска и т.д.
Кодом людям нужно помогать!
Re[4]: Interlocked
От: GlebZ Россия  
Дата: 29.12.18 13:02
Оценка:
Здравствуйте, Sharov, Вы писали:

GZ>>interlocked — это указание компилятору о защищенной команде, что выливается на уровне ассемблера в генерацию префикса lock для текущей команды, например при инкрементации значения с возвратом lock xadd xxx,xxx Чрезвычайно быстрая хрень.


S>Что за дичь написана? Это атомарные инструкции процессора (буквально одна инструкция процессора), а не генерация префиска и т.д.

Не понял? Это команда компилятору на генерацию префикса lock для текущей команды, и эта команда будет выполнена атомарно при взведенном флаге процессора lock. Что тут непонятного?
Re[5]: Interlocked
От: Sharov Россия  
Дата: 29.12.18 13:13
Оценка:
Здравствуйте, GlebZ, Вы писали:

S>>Что за дичь написана? Это атомарные инструкции процессора (буквально одна инструкция процессора), а не генерация префиска и т.д.

GZ>Не понял? Это команда компилятору на генерацию префикса lock для текущей команды, и эта команда будет выполнена атомарно при взведенном флаге процессора lock. Что тут непонятного?

За чем что-то генерировать, когда есть конкретне инсрукции процессора? Это для msil, где про енто можно почитать? Иначе это будет похоже на транзакционную память(или вроде ентого). Т.е. так
можно и FFT атомарно делать.

Еще раз, можно ссылку на генерацию lock и т.д. ибо я себе это нескольок иначе представлял. Вомзожно я неправ.
Кодом людям нужно помогать!
Re[5]: Interlocked
От: RAza  
Дата: 29.12.18 22:18
Оценка:
Здравствуйте, RushDevion, Вы писали:

RD>Как идея: можно возвращать Task. Тогда проброс ошибок будет автоматическим.

Покрутил идею с Task. Итоговый дизайн публичного API мне не нравится. Хочется сохранить условие что данный тип предоставляет потребителям либо значение, либо исключение.

В итоге опираясь на "академический" пример переписал реализацию добавив обработку ошибок. Существующие тесты она проходит, хотя в ближайшее время количество тест кейсов конечно стоит увеличить. Если у вас будет время и возможность посмотрите на код — сильно опасаюсь что я где то накосячил.

public sealed class MyLazy<T>
{
    private volatile Exception error = null;

    private volatile Holder holder = null;

    private volatile int waiting = 0;

    private readonly ManualResetEvent onError = new ManualResetEvent(false);

    private readonly Func<T> getAction = null;

    private readonly bool canBeReseted;

    private int isExist = 0;

    public bool HasValue => holder != null;

    public T Value
    {
        get
        {
            // Предыдущая попытка получить значение вызвала исключение. Все еще имеются ожидающие инициализации потоки. Ждем, пока для каждого будет брошено исключение.
            while (error != null)
            {
                Thread.Sleep(TimeSpan.FromMilliseconds(5));
            }

            var current = holder;

            if (current != null)
                return current.Value;

            if (Interlocked.CompareExchange(ref isExist, 1, 0) == 0)
            {
                try
                {
                    // Захватили право на инициализацию, инициируем
                    holder =
                        new Holder
                            {
                                Value = getAction()
                            };
                }
                catch (Exception ex)
                {
                    Interlocked.Exchange(ref isExist, 0);

                    if (waiting > 0)
                    {
                        onError.Set();

                        error = ex;
                    }

                    throw;
                }

                return holder.Value;
            }

            Interlocked.Increment(ref waiting);

            // Право на инициализацию захватил кто-то другой, ждем, пока инициализация не завершится
            while (true)
            {
                try
                {
                    if (error != null)
                        throw error;

                    var result = holder;

                    if (result != null)
                        return result.Value;

                    onError.WaitOne(TimeSpan.FromMilliseconds(5));
                }
                finally
                {
                    Interlocked.Decrement(ref waiting);

                    if (waiting == 0)
                    {
                        onError.Reset();

                        error = null;
                    }

                }
            }
        }
        set
        {
            Interlocked.Exchange(ref isExist, 1);

            holder =
                new Holder
                    {
                        Value = value
                    };
        }
    }

    public MyLazy(Func<T> getAction, bool canBeReseted)
            : this(getAction)
        => this.canBeReseted = canBeReseted;

    public MyLazy(Func<T> getAction)
        => this.getAction = getAction;

    public void Reset()
    {
        if (canBeReseted == false)
            return;

        Interlocked.Exchange(ref isExist, 0);

        holder = null;
    }

    private class Holder
    {
        public T Value;
    }
}


Из появившихся вопросов:

В чем сакральный смысл копирования ссылки на объект с имеющимся значением в локальную переменную? Для текущего потока это значение попадет в его "локальный кеш"?



Речь про вот этот участок кода:
var current = holder;
if (current != null)
    return current.Value;
Отредактировано 29.12.2018 22:25 RAza . Предыдущая версия .
Re[6]: Interlocked
От: vorona  
Дата: 29.12.18 23:18
Оценка:
Здравствуйте, RAza, Вы писали:

RA>Речь про вот этот участок кода:

RA>
RA>var current = holder;
RA>if (current != null)
RA>    return current.Value;
RA>


It’s important to use the handler local variable, as if instead you access the field twice, it’s possible that the last subscriber will unsubscribe between the check and the invocation:
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.