Обеспечивает ли Interlocked видимость во всех потоках?
-
21-08-2019 - |
Вопрос
Предположим, у меня есть переменная "counter", и есть несколько потоков, обращающихся к "counter" и устанавливающих значение с помощью Interlocked, т.е.:
int value = Interlocked.Increment(ref counter);
и
int value = Interlocked.Decrement(ref counter);
Могу ли я предположить, что изменение, внесенное Interlocked, будет видно во всех потоках?
Если нет, то что мне следует сделать, чтобы все потоки синхронизировали переменную?
Редактировать:кто-то предложил мне использовать volatile .Но когда я устанавливаю "счетчик" как volatile, появляется предупреждение компилятора "ссылка на поле volatile не будет обрабатываться как volatile".
Когда я читал онлайн-справку, там говорилось: "Изменчивое поле обычно не должно передаваться с использованием параметра ref или out".
Решение
Блокированное увеличение / уменьшение на процессорах x86 (добавление блокировки x86 / dec) автоматически создает барьер памяти который обеспечивает видимость для всех потоков (т. Е. все потоки могут видеть его обновление по порядку, например, последовательную согласованность памяти).Барьер памяти приводит к завершению всех ожидающих загрузки / сохранения данных в памяти. volatile
не имеет отношения к этому вопросу, хотя C # и Java (и некоторые компиляторы C / C ++) применяют volatile
чтобы создать барьер памяти.Но заблокированная операция уже имеет барьер памяти со стороны процессора.
Пожалуйста, также взгляните мой другой ответ в stackoverflow.
Обратите внимание, что я предположил, что C # InterlockedIncrement / Decrement являются внутренним отображением для x86 lock add / dec.
Другие советы
Могу ли я предположить, что изменение, внесенное Interlocked, будет видно во всех потоках?
Это зависит от того, как вы считываете значение.Если вы "просто" прочитаете это, то нет, это не всегда будет видно в других потоках, если вы не пометите это как изменяемое.Однако это вызывает раздражающее предупреждение.
В качестве альтернативы (и гораздо предпочтительнее IMO) прочтите его, используя другую взаимосвязанную инструкцию.При этом всегда будет отображаться обновленное значение во всех потоках:
int readvalue = Interlocked.CompareExchange(ref counter, 0, 0);
который возвращает прочитанное значение, и если оно было равно 0, заменяет его на 0.
Мотивация:предупреждение намекает на то, что что-то не так;объединение двух методов (volatile & interlocked) не было предполагаемым способом сделать это.
Обновить:похоже, что другой подход к надежному 32-разрядному чтению без использования "volatile" заключается в использовании Thread.VolatileRead
как было предложено в этот ответ.Есть также некоторые доказательства того, что я совершенно не прав в использовании Interlocked
например, для 32-разрядного чтения эта проблема с подключением, хотя я задаюсь вопросом, не является ли это различие немного педантичным по своей природе.
Что я действительно имею в виду, так это:не используйте этот ответ в качестве единственного источника;У меня есть свои сомнения на этот счет.
На самом деле это не так.Если вы хотите безопасно модифицировать counter
, тогда вы поступаете правильно.Но если вы хотите почитать counter
непосредственно вам нужно объявить это как volatile
.В противном случае у компилятора нет оснований полагать, что counter
изменится, потому что Interlocked
операции находятся в коде, который он может не видеть.
Блокировка гарантирует, что только 1 поток одновременно может обновлять значение.Чтобы гарантировать, что другие потоки смогут прочитать правильное значение (а не кэшированное значение), пометьте его как изменяемое.
общедоступный счетчик изменчивых значений 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, а не аппаратное обеспечение. Ясно, что JIT кэшировал значение переменной terminate [в регистре EAX, и] программа теперь застряла в цикле, выделенном выше..
Либо используя
lock
или добавлениеThread.MemoryBarrier
внутри цикла while проблема будет устранена.Или вы даже можете использоватьVolatile.Read
[илиvolatile
поле]. Целью барьера памяти здесь является только подавление JIT-оптимизации. Теперь, когда мы увидели, как программное и аппаратное обеспечение может изменять порядок операций с памятью, пришло время обсудить барьеры памяти ..
То есть дополнительный барьер конструкция требуется на стороне чтения, чтобы предотвращение проблем с компиляцией и переупорядочением / оптимизацией JIT: это другая проблема, чем когерентность памяти!
Добавление volatile
здесь было бы предотвращать JIT-оптимизацию и, таким образом, "устранить проблему", даже если это приводит к предупреждению.Эта программа также может быть исправлена с помощью Volatile.Read
или одна из различных других операций, которые вызывают барьер:эти барьеры являются такой же частью корректности программы CLR / JIT, как и лежащие в их основе аппаратные барьеры памяти.