Подтвердить что ты не робот

Половина заборов и полных заборов?

Я читал, что Full fences предотвращает любое переупорядочение команд или кеширование вокруг этого ограждения (через памятьBarrier)

Затем я прочитал о volatile, который генерирует "половинные заборы":

Ключевое слово volatile инструктирует компилятор генерировать приобретать ограждение при каждом чтении с этого поля, а также каждая запись в это поле.

acquire-fence

Захват позволяет предотвратить перемещение других операций чтения/записи забор;

release-fence

Заблокировка позволяет предотвратить перемещение других операций чтения/записи после забор.

Может кто-нибудь объяснить мне эти два предложения на простом английском?

(где - забор?)

изменить

После некоторых ответов здесь - я сделал рисунок, который может помочь всем - я думаю.

http://i.stack.imgur.com/A5F7P.jpg enter image description here

4b9b3361

Ответ 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. И вы заметите, что оно реализовано точно так же, как я сделал здесь.

Ответ 2

Начать с другого пути:

Что важно при чтении изменчивого поля? Все предыдущие записи в это поле были зафиксированы.

Что важно при записи в нестабильное поле? Все предыдущие чтения уже получили свои значения.

Затем попробуйте проверить, что забор и освобождение забор имеют смысл в этих ситуациях.

Ответ 3

Чтобы легче рассуждать об этом, позвольте предположить модель памяти, где возможно любое переупорядочение.

Посмотрим на простой пример. Предположим, что это изменчивое поле:

volatile int i = 0;

и эта последовательность чтения-записи:

1. int a = i;
2. i = 3;

Для инструкции 1, которая читается i, создается заборный забор. Это означает, что инструкция 2, которая является записью на i, не может быть переупорядочена с инструкцией 1, поэтому нет возможности, чтобы a было 3 в конце последовательности.

Теперь, конечно, вышесказанное не имеет большого смысла, если вы рассматриваете один поток, но если другой поток должен работать с одними и теми же значениями (предположим, что a является глобальным):

thread 1               thread 2
a = i;                 b = a;
i = 3;

В этом случае вы подумали бы, что нет возможности для потока 2 получить значение 3 в b (так как он получит значение a до или после назначения a = i;). Однако, если считывание и запись i получают переупорядочивание, возможно, что b получает значение 3. В этом случае выполнение i volatile необходимо, если правильность вашей программы зависит от b, не становясь 3.

Отказ от ответственности. Приведенный выше пример предназначен только для теоретических целей. Если компилятор не сошел с ума, он не пойдет и сделает переупорядочивания, которые могли бы создать "неправильное" значение для переменной (т.е. a не могло быть равно 3, даже если i не были энергозависимыми).

Ответ 4

Из volatile (ссылка на С#):

Ключевое слово volatile указывает, что поле может быть изменено несколькими потоками, которые выполняются одновременно.

Чтобы программы работали быстрее, иногда .NET(обычно при оптимизации) выполняет умные вещи, например, не записывает переменную в память, если она будет изменена в следующей команде:

int i = 0;
//Do some stuff.
i++;
//Do some more stuff.
i--;
//Do other stuff.

Здесь компилятор сохранит значение я в регистре до завершения i--;. Это экономит небольшое количество времени, получая значение из ОЗУ.

При поточной передаче это не работает, если я разделен между потоками. Например, у вас может быть:

//Thread 1:
i = 0;      //i is a volatile int shared between threads.
//Do some stuff.
//Wait for Thread 2 to read i.
i++;
//Do some more stuff.
//Wait for Thread 2 to set i = 12.
i--;
//Do other stuff.
//Use i for something like an index.

Если Threads 1 и 2 сохраняют я в регистрах, изменение я в потоке 1 не повлияет на я в потоке 2. Volatile сообщает компилятору, что к этой переменной (i) можно получить доступ из нескольких потоков. В результате он всегда должен получать текущее значение из памяти и историю любых обновленных значений в память.

Другим примером является значение в таблице SQL, где каждый может изменить значение в любое время. Обычные переменные будут похожи на запрос таблицы один раз, а затем используя это значение локально, пока вы не закончите с ним. Волатильные переменные были бы похожи на запрос таблицы, чтобы получить/установить последнее значение каждый раз, когда вам нужно, чтобы каждый имел доступ к текущему значению.

Посмотрите на пример в volatile (ссылка на С#), поскольку он служит хорошим примером использования изменчивых переменных.

Сообщите нам, если вы хотите больше.