Ответ 1
Упомянутая вами формулировка похожа на ту, которую я часто использую. Спецификация говорит об этом, хотя:
- Чтение изменчивого поля называется изменчивым чтением. Неустойчивое чтение имеет "приобретать семантику"; то есть он гарантированно будет происходить до любых ссылок на память, которые происходят после него в последовательности команд.
- Запись изменчивого поля называется volatile write. У изменчивой записи есть "семантика выпуска"; то есть, это гарантировано произойдет после любых ссылок на память до команды записи в последовательности команд.
Но я обычно использую формулировку, указанную в вашем вопросе, потому что я хочу обратить внимание на то, что инструкции могут быть перемещены. Указанная вами формулировка и спецификация эквивалентны.
Я приведу несколько примеров. В этих примерах я собираюсь использовать специальную нотацию, которая использует стрелку ↑, чтобы указать затвор и стрелку ↓, чтобы указать на забор. Никакой другой инструкции не разрешается проплывать мимо стрелки ↑ или вверх по стрелке ↓. Подумайте о том, что голова стрелки отталкивает все от него.
Рассмотрим следующий код.
static int x = 0;
static int y = 0;
static void Main()
{
x++
y++;
}
Переписывая его, чтобы показать, что отдельные инструкции будут выглядеть так.
static void Main()
{
read x into register1
increment register1
write register1 into x
read y into register1
increment register1
write register1 into y
}
Теперь, поскольку в этом примере нет барьеров памяти, компилятор С#, компилятор JIT или оборудование могут оптимизировать его разными способами, если логическая последовательность, воспринимаемая исполняющим потоком, согласуется с физической последовательностью, Вот одна такая оптимизация. Обратите внимание на то, что данные для чтения и записи в/из x
и y
были заменены.
static void Main()
{
read y into register1
read x into register2
increment register1
increment register2
write register1 into y
write register2 into x
}
Теперь это время изменит эти переменные на volatile
. Я буду использовать обозначение стрелки для обозначения барьеров памяти. Обратите внимание, что порядок чтения и записи в/из x
и y
сохраняется. Это объясняется тем, что инструкции не могут двигаться мимо наших барьеров (обозначаются стрелками ↓ и ↑). Теперь это важно. Обратите внимание, что приращение и запись инструкций x
по-прежнему позволяли плавать вниз, а чтение y
всплывало. Это все еще актуально, потому что мы использовали половину заборов.
static volatile int x = 0;
static volatile int y = 0;
static void Main()
{
read x into register1
↓ // volatile read
read y into register2
↓ // volatile read
increment register1
increment register2
↑ // volatile write
write register1 into x
↑ // volatile write
write register2 into y
}
Это очень тривиальный пример. Посмотрите на мой ответ здесь для нетривиального примера того, как volatile
может внести изменения в шаблон с двойной проверкой. Я использую ту же стрелочную нотацию, которую я использовал здесь, чтобы упростить визуализацию происходящего.
Теперь у нас также есть метод Thread.MemoryBarrier
. Он создает полный забор. Поэтому, если мы использовали обозначение стрелки, мы можем визуализировать, как это работает.
Рассмотрим этот пример.
static int x = 0;
static int y = 0;
static void Main
{
x++;
Thread.MemoryBarrier();
y++;
}
Что тогда будет выглядеть, если мы покажем отдельные инструкции, как раньше. Обратите внимание на то, что теперь движение движений запрещено. Нет другого способа, которым это может быть выполнено без ущерба для логической последовательности инструкций.
static void Main()
{
read x into register1
increment register1
write register1 into x
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
read y into register1
increment register1
write register1 into y
}
Хорошо, еще один пример. На этот раз мы будем использовать VB.NET. VB.NET не имеет ключевого слова volatile
. Итак, как мы можем имитировать изменчивое чтение в VB.NET? Мы будем использовать Thread.MemoryBarrier
. 1
Public Function VolatileRead(ByRef address as Integer) as Integer
Dim local = address
Thread.MemoryBarrier()
Return local
End Function
И это то, что похоже на нашу обозначение стрелки.
Public Function VolatileRead(ByRef address as Integer) as Integer
read address into register1
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
return register1
End Function
Важно отметить, что, поскольку мы хотим подражать волатильному считыванию, вызов после Thread.MemoryBarrier
должен быть помещен после фактического чтения. Не попадайте в ловушку, думая, что волатильное чтение означает "свежее чтение", а волатильная запись означает "совершенная запись". Это не так, как это работает, и это, безусловно, не то, что описывает спецификация.
Update:
Относительно изображения.
ждать! Я проверяю, что все записи завершены!
и
ждать! Я проверяю, что все потребители получили текущий значение!
Это ловушка, о которой я говорил. Заявления не совсем точны. Да, барьер памяти, реализованный на аппаратном уровне, может синхронизировать линии когерентности кеша, и в результате вышеприведенные утверждения могут быть в некоторой степени точными сведениями о том, что происходит. Но volatile
не ограничивает движение инструкций. В спецификации указано ничего о загрузке значения из памяти или хранении его в памяти в месте, где находится барьер памяти.
1 Есть, конечно, уже встроенный Thread.VolatileRead
. И вы заметите, что оно реализовано точно так же, как я сделал здесь.