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

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

Тогда я читаю о volatile который генерирует "половину забора":

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

приобретать, забор

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

релиз-забор

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

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

(где забор?)

редактировать

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

4 ответа

Решение

Формулировка, на которую вы ссылаетесь, выглядит так, как я часто использую. Спецификация говорит это, хотя:

  • Чтение изменчивого поля называется изменчивым чтением. Изменчивое чтение имеет "приобретенную семантику"; то есть гарантированно произойдет до любых ссылок на память, которые происходят после нее в последовательности команд.
  • Запись изменяемого поля называется изменяемой записью. Летучая запись имеет "семантику релиза"; то есть это гарантированно произойдет после любых ссылок на память перед инструкцией записи в последовательности команд.

Но я обычно использую формулировку, которую вы цитировали в своем вопросе, потому что я хочу сосредоточиться на том, что инструкции могут быть перемещены. Цитируемая вами формулировка и спецификация эквивалентны.

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

Рассмотрим следующий код.

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
}

Теперь, поскольку в этом примере нет никаких барьеров памяти, компилятор C#, 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 должны быть размещены после фактического чтения. Не попадайтесь в ловушку, думая, что изменчивое чтение означает "свежее чтение", а изменчивое запись означает "совершенную запись". Это не то, как это работает, и, конечно, это не то, что описывается в спецификации.

Обновить:

По ссылке на изображение.

Подождите! Я проверяю, что все писания закончены!

а также

Подождите! Я проверяю, что все потребители получили текущую стоимость!

Это та ловушка, о которой я говорил. Заявления не совсем точны. Да, барьер памяти, реализованный на аппаратном уровне, может синхронизировать строки когерентности кэша, и в результате вышеприведенные утверждения могут быть в некоторой степени точным объяснением того, что происходит. Но, volatile не делает ничего, кроме как ограничить движение инструкций. В спецификации ничего не говорится о загрузке значения из памяти или его сохранении в памяти в том месте, где находится барьер памяти.


1 Есть, конечно, Thread.VolatileRead встроенный уже. И вы заметите, что это реализовано именно так, как я сделал здесь.

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

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

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

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

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

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

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 не были летучими).

Из энергозависимых (C# Reference):

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

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

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

Здесь компилятор будет хранить значение i в регистре до 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.

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

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

Посмотрите на пример в volatile (C# Reference), так как он дает хороший пример того, как использовать переменные.

Дайте нам знать, если вы хотите больше.

Другие вопросы по тегам