Зачем нам нужен Thread.MemoryBarrier ()? - PullRequest
47 голосов
/ 24 августа 2010

В "C # 4 в двух словах" автор показывает, что этот класс может писать 0 иногда без MemoryBarrier, хотя я не могу воспроизвести в моем Core2Duo:

public class Foo
{
    int _answer;
    bool _complete;
    public void A()
    {
        _answer = 123;
        //Thread.MemoryBarrier();    // Barrier 1
        _complete = true;
        //Thread.MemoryBarrier();    // Barrier 2
    }
    public void B()
    {
        //Thread.MemoryBarrier();    // Barrier 3
        if (_complete)
        {
            //Thread.MemoryBarrier();       // Barrier 4
            Console.WriteLine(_answer);
        }
    }
}

private static void ThreadInverteOrdemComandos()
{
    Foo obj = new Foo();

    Task.Factory.StartNew(obj.A);
    Task.Factory.StartNew(obj.B);

    Thread.Sleep(10);
}

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

Вы пытаетесь использовать Барьеры?

Ответы [ 6 ]

66 голосов
/ 24 августа 2010

Вам будет очень трудно воспроизвести эту ошибку. На самом деле, я бы сказал, что вы никогда не сможете воспроизвести его с помощью .NET Framework. Причина в том, что реализация Microsoft использует сильную модель памяти для записи. Это означает, что записи обрабатываются так, как если бы они были изменчивыми. Энергозависимая запись имеет семантику освобождения блокировки, что означает, что все предыдущие записи должны быть зафиксированы до текущей записи.

Однако спецификация ECMA имеет более слабую модель памяти. Таким образом, теоретически возможно, что Mono или даже будущая версия .NET Framework может начать демонстрировать ошибочное поведение.

Итак, я говорю, что очень маловероятно, что устранение барьеров № 1 и № 2 окажет какое-либо влияние на поведение программы. Это, конечно, не гарантия, а наблюдение, основанное только на текущей реализации CLR.

Удаление барьеров № 3 и № 4 определенно окажет влияние. Это на самом деле довольно легко воспроизвести. Ну, не этот пример сам по себе, но следующий код является одной из наиболее известных демонстраций. Он должен быть скомпилирован с использованием сборки Release и запущен вне отладчика. Баг в том, что программа не заканчивается. Вы можете исправить ошибку, поместив вызов на Thread.MemoryBarrier внутри цикла while или пометив stop как volatile.

class Program
{
    static bool stop = false;

    public static void Main(string[] args)
    {
        var t = new Thread(() =>
        {
            Console.WriteLine("thread begin");
            bool toggle = false;
            while (!stop)
            {
                toggle = !toggle;
            }
            Console.WriteLine("thread end");
        });
        t.Start();
        Thread.Sleep(1000);
        stop = true;
        Console.WriteLine("stop = true");
        Console.WriteLine("waiting...");
        t.Join();
    }
}

Причина, по которой некоторые ошибки в потоке трудно воспроизвести, заключается в том, что та же тактика, которую вы используете для симуляции чередования потоков, может фактически исправить ошибку. Thread.Sleep является наиболее заметным примером, потому что он создает барьеры памяти. Вы можете убедиться в этом, поместив вызов внутри цикла while и заметив, что ошибка исчезла.

Вы можете увидеть мой ответ здесь для другого анализа примера из книги, которую вы цитировали.

10 голосов
/ 24 августа 2010

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

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

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

2 голосов
/ 24 августа 2010

Очень сложно воспроизвести многопоточные ошибки - обычно вам приходится запускать тестовый код много раз (тысячи) и иметь некоторую автоматическую проверку, которая помечает, если ошибка возникает. Вы можете попытаться добавить короткий Thread.Sleep (10) между некоторыми строками, но опять же это не всегда гарантирует, что вы получите те же проблемы, что и без него.

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

2 голосов
/ 24 августа 2010

Если вы используете volatile и lock, барьер памяти встроен. Но да, он вам нужен в противном случае.Сказав это, я подозреваю, что вам нужно вдвое меньше, чем показывает ваш пример.

1 голос
/ 21 сентября 2017

Я просто процитирую одну из замечательных статей о многопоточности:

Рассмотрим следующий пример:

class Foo
{
  int _answer;
  bool _complete;

  void A()
  {
    _answer = 123;
    _complete = true;
  }

  void B()
  {
    if (_complete) Console.WriteLine (_answer);
  }
}

Если методы A и B выполнялись одновременно в разных потоках, это может быть возможно ли для B написать «0»? Ответ да - для следующего Причины:

Компилятор, CLR или CPU могут переупорядочить инструкции вашей программы в повысить эффективность. Компилятор, CLR или CPU могут вводить кеширование оптимизации, так что назначения переменных не будут видны другие темы сразу. C # и время выполнения очень осторожны убедитесь, что такие оптимизации не нарушают обычную однопоточность код - или многопоточный код, который правильно использует блокировки. за пределами из этих сценариев, вы должны явно победить эти оптимизации путем создание барьеров памяти (также называемых ограждения памяти), чтобы ограничить эффекты переупорядочения инструкций и кэширования чтения / записи.

Полные заборы

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

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

Вся теория, лежащая в основе Thread.MemoryBarrier, и почему нам нужно использовать ее в неблокирующих сценариях, чтобы сделать код безопасным и надежным, хорошо описана здесь: http://www.albahari.com/threading/part4.aspx

0 голосов
/ 24 августа 2010

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

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

...