Обеспечивает ли блокировка видимость во всех потоках?
Предположим, что у меня есть переменная "счетчик", и есть несколько потоков, обращающихся и устанавливающих значение "счетчика" с помощью блокировки, т. е.:
int value = Interlocked.Increment(ref counter);
И
int value = Interlocked.Decrement(ref counter);
Могу ли я предположить, что изменение, внесенное блокируемым, будет видно во всех потоках?
Если нет, то что я должен сделать, чтобы все потоки синхронизировали переменную?
EDIT: кто-то предложил мне использовать volatile. Но когда я устанавливаю "счетчик" как volatile, появляется предупреждение компилятора " ссылка на летучее поле не будет рассматриваться как летучее".
Когда я читал онлайн-справку, в ней говорилось: "волатильное поле обычно не должно передаваться с помощью параметра ref или out".
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, как и лежащие в основе аппаратные барьеры памяти.