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

Thread.VolatileRead() vs Volatile.Read()

Нам говорят, что Volatile.Read над Thread.VolatileRead в большинстве случаев из-за того, что последний испускает полный забор, а первый испускает только соответствующую половину ограждения (например, приобретает забор); который более эффективен.

Однако, по моему мнению, Thread.VolatileRead фактически предлагает что-то, что Volatile.Read не делает, из-за реализации Thread.VolatileRead:

public static int VolatileRead(ref int address) {
  int num = address;
  Thread.MemoryBarrier();
  return num;
}

Из-за полного барьера памяти во второй строке реализации, я считаю, что VolatileRead фактически гарантирует, что значение, которое было записано последним на address, будет считано. Согласно Wikipedia, "Полный забор гарантирует, что все операции загрузки и хранения до забора будут зафиксированы до любых нагрузок и хранилищ выпущенный после ограждения".

Правильно ли я понимаю? И поэтому, Thread.VolatileRead все еще предлагает что-то, что Volatile.Read не делает?

4b9b3361

Ответ 1

Возможно, я немного опоздал на игру, но мне все равно хотелось бы перезвонить. Сначала нам нужно согласовать некоторые базовые определения.

  • Захват: барьер памяти, в котором другие чтения и записи не могут перемещаться перед забором.
  • релиз-забор: барьер памяти, в котором другие чтения и записи не могут перемещаться после забора.

Мне нравится использовать обозначение стрелки, чтобы помочь проиллюстрировать заборы в действии. Стрелка ↑ будет представлять собой забор, а стрелка ↓ будет представлять собой забор. Подумайте о том, что стрелка направлена ​​на то, чтобы отжать память в направлении стрелки. Но, и это важно, доступ к памяти может перемещаться за хвост. Прочтите определения упомянутых выше ограждений и убедитесь, что стрелки визуально представляют эти определения.

Используя эту нотацию, давайте проанализируем примеры из ответа JaredPar, начиная с Volatile.Read. Но сначала позвольте мне сказать, что Console.WriteLine, вероятно, создает барьер с полным заграждением, который нам неизвестен. Мы должны притвориться на мгновение, что это не облегчает последующие примеры. На самом деле, я просто полностью опускаю вызов, поскольку он не нужен в контексте того, чего мы пытаемся достичь.

// Example using Volatile.Read
x = 13;
var local = y; // Volatile.Read
↓              // acquire-fence
z = 13;

Таким образом, используя обозначение стрелки, мы более легко видим, что запись в z не может перемещаться вверх и перед чтением y. Нельзя читать y вниз и после записи z, потому что это будет так же, как и наоборот. Другими словами, он блокирует относительное упорядочение y и z. Однако чтение y и запись на x можно поменять местами, поскольку стрелка не предотвращает движение. Аналогично, запись в x может перемещаться за хвост стрелки и даже минус запись на z. Технически это спецификация допускает... в любом случае. Это означает, что мы имеем следующие допустимые порядки.

Volatile.Read
---------------------------------------
write x    |    read y     |    read y
read y     |    write x    |    write z
write z    |    write z    |    write x

Теперь перейдем к примеру с помощью Thread.VolatileRead. Для примера я сделаю запрос Thread.VolatileRead, чтобы упростить визуализацию.

// Example using Thread.VolatileRead
x = 13;
var local = y; // inside Thread.VolatileRead
↑              // Thread.MemoryBarrier / release-fence
↓              // Thread.MemoryBarrier / acquire-fence
z = 13;

Посмотрите внимательно. Нет стрелки (потому что нет барьера памяти) между записью на x и чтением y. Это означает, что эти обращения к памяти по-прежнему могут перемещаться относительно друг друга. Тем не менее, вызов Thread.MemoryBarrier, который создает дополнительную блокировку, делает ее видимой, как будто следующий доступ к памяти имеет изменчивую семантику записи. Это означает, что записи в x и z больше не могут быть заменены.

Thread.VolatileRead
-----------------------
write x    |    read y
read y     |    write x
write z    |    write z

Конечно, утверждается, что реализация Microsoft CLI (.NET Framework) и аппаратного обеспечения x86 уже гарантирует семантику освобождения для всех записей. Таким образом, в этом случае между двумя вызовами не может быть никакой разницы. На процессоре ARM с моно? В этом случае все может быть разным.

Перейдем к вашим вопросам.

Из-за полного барьера памяти на второй строке Я считаю, что VolatileRead фактически гарантирует, что значение, записанное последним на адрес, будет считано. Я понимаю правильно?

Нет. Это неверно! Неустойчивое чтение - это не то же самое, что "свежее чтение". Зачем? Это связано с тем, что после инструкции считывания помещается барьер памяти. Это означает, что фактическое чтение по-прежнему свободно перемещаться вверх или назад во времени. Другой поток мог бы написать адрес, но текущий поток, возможно, уже переместил чтение в точку во времени до того, как другой поток зафиксировал это.

Таким образом, возникает вопрос: "Почему люди беспокоятся о том, чтобы использовать изменчивые чтения, если они кажутся так мало?". Ответ заключается в том, что он абсолютно гарантирует, что следующее чтение будет более новым, чем предыдущее чтение. Это его ценность! Вот почему много незакрепленного кода вращается в цикле, пока логика не сможет определить, что операция была успешно завершена. Другими словами, код без блокировки использует концепцию, согласно которой более поздний, прочитанный в последовательности многих чтений, вернет новое значение, но код не должен предполагать, что любое из чтений обязательно представляет последнее значение.

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

Но, рассмотрев предостережения о том, что это означает даже "прочитанное", прочитанное выше, вы все же хотите что-то, что действует как "свежее" чтение, тогда вам нужно будет установить забор перед чтением. Обратите внимание, что это явно не то же самое, что и волатильное чтение, но оно лучше соответствует интуиции разработчиков того, что означает "свежий". Однако термин "свежий" в случае не является абсолютным. Вместо этого чтение является "свежим" относительно барьера. То есть он не может быть старше, чем момент времени, в который был выполнен барьер. Но, как уже упоминалось выше, значение может не соответствовать последнему значению к моменту использования или принятия решения на его основе. Просто имейте это в виду.

И поэтому, Thread.VolatileRead все еще предлагает что-то, что Volatile.Read нет?

Да. Я думаю, JaredPar представил прекрасный пример случая, когда он может предложить что-то дополнительное.

Ответ 2

Volatile.Read по существу гарантирует выполнение операций чтения и записи, которые происходят после того, как они не могут быть перемещены перед чтением. В нем ничего не говорится о предотвращении операций записи, которые происходят до того, как чтение переходит из него. Например

// assume x, y and z are declared 
x = 13;
Console.WriteLine(Volatile.Read(ref y));
z = 13;

Нет гарантии, что запись в x происходит до чтения y. Однако запись в z гарантируется после чтения y.

// assume x, y and z are declared 
x = 13;
Console.WriteLine(Thread.VolatileRead(ref y));
z = 13;

В этом случае, хотя вы можете быть уверены, что порядок здесь

  • написать x
  • прочитайте y
  • написать z

Полный забор предотвращает перемещение между чтением и записью в любом направлении