Volatile и Thread.MemoryBarrier в C # - PullRequest
       60

Volatile и Thread.MemoryBarrier в C #

9 голосов
/ 24 ноября 2010

Для реализации кода без блокировки для многопоточного приложения я использовал volatile переменные, Теоретически : ключевое слово volatile просто используется, чтобы убедиться, что все потоки видят самое последнее значение изменчивой переменной; поэтому, если поток A обновляет значение переменной, а поток B читает эту переменную сразу после того, как произойдет это обновление, он увидит самое обновленное значение, записанное недавно из потока А. Как я читал в C # 4.0 в книге «В двух словах» , неверно , потому что

применение volatile не предотвращает замену записи, за которой следует чтение.

Можно ли решить эту проблему, поместив Thread.MemoryBarrier() перед каждым получением переменной volatile, например:

private volatile bool _foo = false;

private void A()
{
    //…
    Thread.MemoryBarrier();
    if (_foo)
    {
        //do somthing
    }
}

private void B()
{
    //…
    _foo = true;
    //…
}

И если это решит проблему; предположим, что у нас есть цикл while, который зависит от этого значения в одном из его условий; правильно ли ставить Thread.MemoryBarrier() перед циклом while, чтобы исправить проблему? Пример:

private void A()
{
    Thread.MemoryBarrier();
    while (_someOtherConditions && _foo)
    {
        // do somthing.
    }
}

Чтобы быть более точным, я хочу, чтобы переменная _foo давала самое свежее значение, когда любой поток запрашивал ее в любое время; так что если вставка Thread.MemoryBarrier() перед вызовом переменной решит проблему, тогда я мог бы использовать свойство Foo вместо _foo и сделать Thread.MemoryBarrier() в пределах get этого свойства.

Foo
{
    get 
    {
        Thread.MemoryBarrier();
        return _foo;
    }
    set
    {
        _foo = value;
    }
}

Ответы [ 5 ]

9 голосов
/ 24 ноября 2010

«C # In Nutshell» является правильным, но его утверждение является спорным.Зачем?

  • «запись» с последующим «чтением», без «volatile», гарантированно произойдет в любом случае в порядке программы , если она воздействует на логику в пределах одного потока
  • «Запись» перед «чтением» в многопоточной программе совершенно бессмысленно , о которой следует беспокоиться в вашем примере.

Давайте уточним.Возьмите исходный код:

private void A() 
{ 
    //… 
    if (_foo) 
    { 
        //do something 
    } 
}

Что произойдет, если планировщик потока уже проверил переменную _foo, но она приостановлена ​​как раз перед комментарием //do something?Что ж, в этот момент ваш другой поток может изменить значение _foo, что означает, что все ваши летучие компоненты и Thread.MemoryBarriers не имеют значения !!!Если абсолютно необходимо избегать do_something, если значение _foo ложно, то у вас нет другого выбора, кроме как использовать блокировку.

Однако, если это нормально для do somethingвыполнять, когда внезапно _foo становится ложным, тогда это означает, что ключевого слова volatile было более чем достаточно для ваших нужд.

Чтобы было ясно: все респонденты, которые говорят вам использовать барьер памяти, неверны илиобеспечиваем перебор.

5 голосов
/ 24 ноября 2010

Книга правильная .
Модель памяти CLR указывает, что операции загрузки и сохранения могут быть переупорядочены. Это относится к энергозависимым и энергонезависимым переменным.

Объявление переменной как volatile означает лишь, что операции загрузки будут иметь получение семантики, а операции хранения будут иметь release семантику. Кроме того, компилятор будет избегать выполнения некоторых оптимизаций, которые передают тот факт, что переменная доступна однопоточным образом (например, поднятие нагрузки / сохранение вне циклов).

Использование одного ключевого слова volatile не создает критических разделов и не вызывает магической синхронизации потоков друг с другом.

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

0 голосов
/ 24 ноября 2010

Вытащено с здесь ...

class Foo
{
  int _answer;
  bool _complete;

  void A()
  {
    _answer = 123;
    Thread.MemoryBarrier();    // Barrier 1
    _complete = true;
    Thread.MemoryBarrier();    // Barrier 2
  }

  void B()
  {
    Thread.MemoryBarrier();    // Barrier 3
    if (_complete)
    {
      Thread.MemoryBarrier();       // Barrier 4
      Console.WriteLine (_answer);
    }
  }
}

Барьеры 1 и 4 предотвращают этот пример от написания «0». Барьеры 2 и 3 обеспечить гарантию свежести: они убедитесь, что если B побежал за A, читая _complete будет оцениваться как true.

Так что, если мы вернемся к вашему циклическому примеру ... это должно выглядеть так ...

private void A()
{
    Thread.MemoryBarrier();
    while (_someOtherConditions && _foo)
    {
        //do somthing
        Thread.MemoryBarrier();
    }
}
0 голосов
/ 24 ноября 2010

Слова Microsoft о барьерах памяти:

MemoryBarrier требуется только в многопроцессорных системах со слабым упорядочением памяти (например, в системе, использующей несколько процессоров Intel Itanium).В большинстве случаев оператор блокировки C #, оператор Visual Basic SyncLock или класс Monitor предоставляют более простые способы синхронизации данных.

0 голосов
/ 24 ноября 2010

Во втором примере вам также необходимо поместить Thread.MemoryBarrier(); внутри цикла, чтобы убедиться, что вы получаете самое последнее значение каждый раз, когда проверяете состояние цикла.

...