Обеспечивает ли блокировка видимость во всех потоках?


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

int value = Interlocked.Increment(ref counter);

И

int value = Interlocked.Decrement(ref counter);

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

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

EDIT: кто-то предложил мне использовать volatile. Но когда я устанавливаю "счетчик" как volatile, появляется предупреждение компилятора " ссылка на летучее поле не будет рассматриваться как летучее".

Когда я читал онлайн-справку, в ней говорилось: "волатильное поле обычно не должно передаваться с помощью параметра ref или out".

5 7

5 ответов:

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

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

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

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

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

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

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

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

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

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

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

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

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

Счетчик Public volatile int;

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

Вот пример кода, который никогда не завершится из-за 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 кэшировал значение переменной terminate [в регистре EAX, и теперь программа застряла в цикле, выделенном выше ..

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

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

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