Подтвердить что ты не робот

Зачем нам нужен Thread.MemoryBarrier()?

В "С# 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);
}

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

Вы беспокоитесь об использовании барьеров?

4b9b3361

Ответ 1

Вам будет очень сложно воспроизвести эту ошибку. Фактически, я бы сказал, что вы никогда не сможете воспроизвести его с помощью .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 и заметив, что ошибка исчезла.

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

Ответ 2

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

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

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

Ответ 3

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

Ответ 4

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

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

Ответ 5

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

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

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 могут ввести кэширование таким образом, чтобы присвоения переменных не были видны другие темы сразу. С# и время выполнения очень осторожны убедитесь, что такие оптимизации не разбивают обычные однопоточные код или многопоточный код, который правильно использует блокировки. за пределами этих сценариев, вы должны явно проигнорировать эти оптимизации создание барьеров памяти (также называемых заборами памяти), чтобы ограничить эффекты переупорядочения команд и кэширование чтения/записи.

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

Самый простой вид барьера памяти - полная память барьер (полный забор), который предотвращает любое переупорядочение команд или кеширование вокруг этого забора. Вызов 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

Ответ 6

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

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