Обеспечивает ли Interlocked видимость во всех потоках? - PullRequest
7 голосов
/ 05 февраля 2009

Предположим, у меня есть переменная "counter", и есть несколько потоков, которые получают доступ и устанавливают значение "counter" с помощью Interlocked, т.е.

int value = Interlocked.Increment(ref counter);

и

int value = Interlocked.Decrement(ref counter);

Можно ли предположить, что изменения, сделанные Interlocked, будут видны во всех потоках?

Если нет, что мне делать, чтобы все потоки синхронизировали переменную?

РЕДАКТИРОВАТЬ: кто-то предложил мне использовать volatile. Но когда я устанавливаю «счетчик» как volatile, появляется предупреждение компилятора «ссылка на volatile поле не будет рассматриваться как volatile».

Когда я читал интерактивную справку, он сказал: «Обычно изменяемое поле не должно передаваться с использованием параметра ref или out».

Ответы [ 5 ]

6 голосов
/ 26 января 2010

InterlockedIncrement / Decrement на процессорах x86 (блокировка add / dec в x86) автоматически создает барьер памяти , который обеспечивает видимость для всех потоков (т. Е. Все потоки могут видеть свое обновление в порядке, как последовательная память консистенция). Барьер памяти делает все ожидающие загрузки / сохранения памяти для завершения. volatile не имеет отношения к этому вопросу, хотя C # и Java (и некоторые компиляторы C / C ++) заставляют volatile создавать барьер памяти. Но блокированная операция уже имеет барьер памяти процессором.

Пожалуйста, посмотрите также мой другой ответ в stackoverflow.

Обратите внимание, что я предположил, что InterlockedIncrement / Decrement в C # являются внутренним отображением блокировки x86 add / dec.

6 голосов
/ 26 января 2010

Можно ли предположить, что изменения, сделанные Interlocked, будут видны во всех потоках?

Это зависит от того, как вы читаете значение. Если вы «просто» прочитали его, то нет, это не всегда будет видно в других потоках, если вы не отметите его как изменчивый. Это вызывает раздражающее предупреждение, хотя.

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

int readvalue = Interlocked.CompareExchange(ref counter, 0, 0);

, который возвращает прочитанное значение, и, если оно было 0, заменяет его на 0.

Мотивация: предупреждение намекает, что что-то не так; объединение двух методов (изменчивых и взаимосвязанных) не было предназначенным способом сделать это.

Обновление: похоже, что другой подход к надежному 32-битному чтению без использования volatile - это использование Thread.VolatileRead, как предложено в этого ответа . Также есть некоторые свидетельства того, что я совершенно не прав в использовании Interlocked для 32-битных операций чтения, например в этой проблеме Connect , хотя мне интересно, является ли различие немного педантичным по своей природе.

Что я действительно имею в виду: не используйте этот ответ в качестве единственного источника; У меня есть сомнения по этому поводу.

3 голосов
/ 05 февраля 2009

На самом деле это не так. Если вы хотите безопасно изменить counter, то вы делаете правильную вещь. Но если вы хотите прочитать counter напрямую, вам нужно объявить его как volatile. В противном случае у компилятора нет оснований полагать, что counter изменится, поскольку операции Interlocked находятся в коде, который он может не увидеть.

1 голос
/ 04 июня 2018

Нет; только для блокировки только при записи не гарантирует, что чтение переменных в коде действительно свежее; программа, которая также неправильно читает из поля , может не быть потокобезопасным даже под «сильной моделью памяти». Это относится к любой форме присвоения полю, совместно используемому потоками.

Вот пример кода, который никогда не завершится из-за JIT . (Он был изменен с Барьеры памяти в .NET , чтобы быть работающей программой LINQPad, обновленной для вопроса).

// Run this as a LINQPad program in "Release Mode".
// ~ It will never terminate on .NET 4.5.2 / x64. ~
// The program will terminate in "Debug Mode" and may terminate
// in other CLR runtimes and architecture targets.
class X {
    // Adding {volatile} would 'fix the problem', as it prevents the JIT
    // optimization that results in the non-terminating code.
    public int terminate = 0;
    public int y;

    public void Run() {
        var r = new ManualResetEvent(false);
        var t = new Thread(() => {
            int x = 0;
            r.Set();
            // Using Volatile.Read or otherwise establishing
            // an Acquire Barrier would disable the 'bad' optimization.
            while(terminate == 0){x = x * 2;}
            y = x;
        });

        t.Start();
        r.WaitOne();
        Interlocked.Increment(ref terminate);
        t.Join();
        Console.WriteLine("Done: " + y);
    }
}

void Main()
{
    new X().Run();
}

Объяснение из Барьеры памяти в .NET :

На этот раз это JIT, а не аппаратное обеспечение. Понятно, что JIT кэшировал значение переменной terminate [в регистре EAX и программа] теперь застряла в цикле, выделенном выше ..

Либо использование lock, либо добавление Thread.MemoryBarrier внутри цикла while решит проблему. Или вы можете даже использовать Volatile.Read [или volatile поле]. Цель барьера памяти здесь состоит только в подавлении JIT-оптимизации. Теперь, когда мы увидели, как программное и аппаратное обеспечение может переупорядочивать операции с памятью , пришло время обсудить барьеры памяти ..

То есть требуется дополнительная конструкция барьер на стороне чтения для предотвращения проблем с компиляцией и повторным упорядочением / оптимизацией JIT : это другая проблема, чем согласованность памяти!

Добавление volatile здесь предотвратит оптимизацию JIT и, таким образом, "решит проблему", даже если это приведет к предупреждению. Эту программу также можно исправить с помощью Volatile.Read или одной из различных других операций, которые создают барьер: эти барьеры являются такой же частью правильности программы CLR / JIT, как и основные аппаратные ограждения памяти.

1 голос
/ 05 февраля 2009

Interlocked гарантирует, что только 1 поток за раз может обновить значение. Чтобы другие потоки могли прочитать правильное значение (а не кэшированное значение), пометьте его как энергозависимое.

public volatile int Counter;

...