Начнем с некоторых определений. Ключевое слово volatile
создает блокировку получения при чтении и блокировку выпуска при записи. Они определены следующим образом.
- Acqua-забор: Барьер памяти, в котором другие операции чтения и записи не могут перемещаться перед забором.
- релиз-забор: барьер памяти, в котором другие операции чтения и записи не могут перемещаться после ограждения.
Метод Thread.MemoryBarrier
создает полное ограждение. Это означает, что он создает и блок-забор, и забор-релиз. К сожалению, MSDN говорит об этом.
Синхронизирует доступ к памяти следующим образом: процессор, выполняющий текущий поток, не может переупорядочить инструкции таким образом, чтобы обращения к памяти до вызова MemoryBarrier выполнялись после обращений к памяти, следующих за вызовом MemoryBarrier.
Интерпретация этого приводит нас к мысли, что это только создает ограничение выпуска. Так что это? Полный забор или полузабор? Это, наверное, тема для другого вопроса. Я буду работать в предположении, что это полный забор, потому что многие умные люди сделали это заявление. Но, что более убедительно, сам BCL использует Thread.MemoryBarrier
, как если бы он производил полный забор. Так что в этом случае документация, вероятно, неверна. Что еще более забавно, это утверждение фактически подразумевает, что инструкции перед вызовом могут быть каким-то образом зажаты между вызовом и инструкциями после него. Это было бы абсурдно. Я говорю это в шутку (но не совсем), что для Microsoft было бы полезно, чтобы юрист просматривал всю документацию, касающуюся потоковой передачи. Я уверен, что их юридические навыки могут найти хорошее применение в этой области.
Теперь я собираюсь ввести обозначения стрелок, чтобы помочь проиллюстрировать заборы в действии. Стрелка ↑ обозначает ограничитель выпуска, а стрелка ↓ - ограничитель получения. Представьте, что острие стрелки отодвигает доступ к памяти в направлении стрелки. Но, и это важно, доступ к памяти может проходить мимо хвоста. Прочтите определения ограждений выше и убедитесь, что стрелки наглядно представляют эти определения.
Далее мы проанализируем цикл только потому, что это самая важная часть кода. Для этого я собираюсь раскрутить цикл. Вот как это выглядит.
LOOP_TOP:
// Iteration 1
read stop into register
jump-if-true to LOOP_BOTTOM
↑
full-fence // via Thread.MemoryBarrier
↓
read toggle into register
negate register
write register to toggle
goto LOOP_TOP
// Iteration 2
read stop into register
jump-if-true to LOOP_BOTTOM
↑
full-fence // via Thread.MemoryBarrier
↓
read toggle into register
negate register
write register to toggle
goto LOOP_TOP
...
// Iteration N
read stop into register
jump-if-true to LOOP_BOTTOM
↑
full-fence // via Thread.MemoryBarrier
↓
read toggle into register
negate register
write register to toggle
goto LOOP_TOP
LOOP_BOTTOM:
Обратите внимание, что вызов Thread.MemoryBarrier
ограничивает перемещение некоторой части доступа к памяти. Например, чтение toggle
не может перемещаться перед чтением stop
или наоборот, потому что доступ к памяти не может перемещаться по стрелке.
А теперь представьте, что произойдет, если убрать полное ограждение. Компилятор C #, JIT-компилятор или оборудование теперь имеют гораздо больше свободы в перемещении инструкций. В частности, теперь разрешена оптимизация подъема, официально известная как инвариантное движение кода цикла. В основном компилятор обнаруживает, что stop
никогда не изменяется, и поэтому чтение выходит из цикла. Теперь он эффективно кэшируется в реестр. Если бы барьер памяти был на месте, то при чтении пришлось бы проталкиваться вверх через стрелку, а спецификация специально это не допускает. Это намного легче визуализировать, если размотать цикл, как я сделал выше. Помните, что вызов Thread.MemoryBarrier
будет происходить на каждой итерации цикла, поэтому вы не можете просто сделать выводы о том, что произойдет, только на одной итерации.
Проницательный читатель заметит, что компилятор может менять местами чтение toggle
и stop
таким образом, что stop
"обновляется" в конце цикла, а не в начале, но это не имеет отношения к контекстному поведению цикла. . Он имеет ту же семантику и дает тот же результат.
У меня вопрос: почему без добавления Thread.MemoryBarrier () или даже Console.WriteLine () в цикл while проблема решается?
Поскольку барьер памяти накладывает ограничения на оптимизацию, которую может выполнять компилятор. Это запретило бы движение инвариантного кода цикла. Предполагается, что Console.WriteLine
создает барьер памяти, что, вероятно, верно. Без барьера памяти компилятор C #, JIT-компилятор или оборудование могут свободно поднять чтение stop
за пределы самого цикла.
Я предполагаю, что, поскольку на многопроцессорной машине поток работает со своим собственным кешем значений и никогда не извлекает обновленное значение остановки, потому что оно имеет свое значение в кеше?
Короче ... да. Но учтите, что это никак не связано с количеством процессоров. Это можно продемонстрировать на одном процессоре.
Или основной поток не сохраняет это в памяти?
Нет. Основной поток зафиксирует запись. Вызов Thread.Join
гарантирует это, потому что он создаст барьер памяти, который не позволяет перемещению записи упасть ниже соединения.
Также почему Console.WriteLine () исправляет это? Это потому, что он также реализует MemoryBarrier?
да. Вероятно, это создает барьер памяти. Я храню список генераторов барьеров памяти здесь.
person
Brian Gideon
schedule
30.07.2014