Обеспечивает ли блокировка видимость во всех потоках?
Предположим, что у меня есть переменная "счетчик", и есть несколько потоков, обращающихся и устанавливающих значение "счетчика" с помощью блокировки, т. е.:
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, как и лежащие в основе аппаратные барьеры памяти.