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

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

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

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

47
задан Adi Lester 14 August 2012 в 20:57
поделиться

5 ответов

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

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

66
ответ дан 26 November 2019 в 19:43
поделиться

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

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

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

10
ответ дан 26 November 2019 в 19:43
поделиться

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

2
ответ дан 26 November 2019 в 19:43
поделиться

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

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

2
ответ дан 26 November 2019 в 19:43
поделиться

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

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

0
ответ дан 26 November 2019 в 19:43
поделиться
Другие вопросы по тегам:

Похожие вопросы: