Барьеры потоковой передачи и неявной памяти

Пытаемся понять модель памяти .net, когда дело касается потоковой передачи. Это чисто теоретический вопрос, и я знаю, что его можно решить другими способами, например, используя lock или пометив _task как volatile.

Возьмем, к примеру, следующий фрагмент кода:

class Test
{
    Task _task;
    int _working = 0;

    public void Run()
    {
        if (Interlocked.CompareExchange(ref _working, 1, 0) == 0)
        {
            _task = Task.Factory.StartNew(() =>
            {
                //do some work...
            });
            _task.ContinueWith(antecendent => Interlocked.Exchange(ref _working, 0));
        }
    }

    public void Dispose()
    {
        if (Interlocked.CompareExchange(ref _working, _working, 0) == 1)
        {
            _task.ContinueWith(antecendent => { /*do some other work*/ });
        }
    }
}

Теперь сделайте следующие предположения:

  1. Run может вызываться несколько раз (из разных потоков) и никогда не будет вызван после вызова Dispose.
  2. Dispose будет вызываться ровно один раз.

Теперь к моему вопросу, всегда ли значение _task (в методе Dispose) будет «свежим» значением, то есть будет ли оно считываться из «основной памяти», а не из регистра? Из того, что я читал, Interlocked создает полный барьер памяти, поэтому я предполагаю, что _task будет считываться из основной памяти, или я полностью отключен?


person Tsef    schedule 26.05.2014    source источник
comment
Нет особых причин предполагать, что какой-либо код изменяет переменную _task после своего запуска. Переменные, которые только когда-либо считываются, не могут иметь неправильное значение. Большим преимуществом использования оператора lock является то, что его намного легче продумать. У вас были бы некоторые шансы избежать ошибки, связанной с необнаруживаемой гонкой потоков, которую вы вставили в свой код. У вас нет никакой гарантии, что задача действительно будет продолжена добавленной задачей.   -  person Hans Passant    schedule 26.05.2014
comment
Читая ваш ответ, я предполагаю, что вы действительно не читали мой вопрос. Прежде всего, переменная _task может измениться, если Run был вызван еще раз (посмотрите продолжение в методе Run). Мне хорошо известно, что использование блокировки было бы проще для отладки, чтения и т. Д., И я бы не стал использовать такой код, как приведенный выше, в реальном сценарии. Однако это всего лишь пример того, что я пытаюсь вникнуть глубже, а именно всей модели барьера памяти.   -  person Tsef    schedule 26.05.2014
comment
Я бы не стал называть эту модель памяти .NET. Барьеры сводятся к инструкциям ассемблера - синхронность кэша между кешами ядра не совсем проблема .NET.   -  person TomTom    schedule 29.05.2014
comment
@TomTom Язык C # дает определенные гарантии того, как будут вести себя заданные переменные, которые вы можете назвать моделью памяти языков. Пока, согласно правилам спецификации языка, код должен работать, вам действительно не нужно заботиться о деталях того, как язык C # обеспечивает соблюдение ограничений своей модели памяти. То есть, если вы не обнаружите ошибку в языке C #, но, как правило, вы не ожидаете, что с ней будет очень часто сталкиваться.   -  person Servy    schedule 29.05.2014
comment
Из Threading in C # Джо Альбахари: Все методы Interlocked генерируют полный забор. Следовательно, поля, к которым вы обращаетесь через Interlocked, не нуждаются в дополнительных ограждениях - если они не доступны в других местах вашей программы без Interlocked или блокировки. albahari.com/threading/part4.aspx#_Nonblocking_Synchronization   -  person Kris Vandermotten    schedule 29.05.2014
comment
Я тоже читал эту книгу и понимаю это, но доступ к _task через interlocked не выполнялся.   -  person Tsef    schedule 01.06.2014


Ответы (2)


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

public void Dispose()
{
    int register = _working;
    if (Interlocked.CompareExchange(ref _working, register, 0) == 1)
    {
        _task.ContinueWith(antecendent => { /*do some other work*/ });
    }
}

Второй параметр CompareExchange передается по значению, чтобы его можно было кэшировать в регистре. Я представляю себе следующий сценарий.

  • Поток A вызывает Run
  • Поток A делает что-то еще с _working, заставляя его кэшировать его в регистре.
  • Поток B завершает задачу и вызывает Exchange от делегата ContinueWith.
  • Поток A вызывает Dispose.

В приведенном выше сценарии _working изменится на 1, затем на 0, а затем Dispose, вернув его обратно на 1 (потому что это значение было кэшировано в регистре), даже не входя в оператор if. В этот момент _working может находиться в несогласованном состоянии.

Лично я думаю, что этот сценарий маловероятен, в основном потому, что я не думаю, что _working будет кэшироваться таким образом, особенно если вы всегда обеспечиваете защиту доступа к нему с помощью заблокированных операций.

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

person Brian Gideon    schedule 29.05.2014
comment
Я понимаю вашу точку зрения и полностью с вами согласен. Я бы не стал использовать такой код в сценарии реального мира, если бы я этого не делал, потому что это немного усложняет понимание / чтение. Я хотел лучше понять случай, который, как мне кажется, у меня есть. Спасибо за ваш вклад. - person Tsef; 01.06.2014
comment
@zaf: Я с тобой. Я тоже не особо пользуюсь lock-free. Но в любом случае есть много преимуществ в понимании того, как все работает за кулисами. Мне еще предстоит многому научиться. - person Brian Gideon; 01.06.2014

Я не пишу код на C #, но если используется полный барьер памяти, то ваша интерпретация верна. Компилятор не должен повторно использовать значение, хранящееся в регистрах, а скорее извлекать его таким образом, чтобы барьеры упорядочивания памяти не маскировали фактическое значение, присутствующее в подсистеме памяти.

Я также нашел этот ответ, который четко объясняет, что это действительно так, поэтому документация, которую вы прочитали, кажется правильной: Использует ли Interlocked.CompareExchange барьер памяти?

person Alexandros    schedule 27.05.2014