Где размещать ограждения/барьеры памяти, чтобы гарантировать свежие чтения/записи?
Как и многие другие люди, меня всегда путают волатильные чтения/записи и заборы. Итак, теперь я пытаюсь полностью понять, что они делают.
Таким образом, волатильное чтение должно (1) демонстрировать семантику получения и (2) гарантировать, что прочитанное значение является свежим, то есть оно не является кешированным значением. Пусть сосредоточиться на (2).
Теперь, я прочитал, что, если вы хотите выполнить изменчивое чтение, вам следует ввести забор (или полный забор) после чтения, например:
int local = shared;
Thread.MemoryBarrier();
Как именно это мешает операции чтения использовать ранее кэшированное значение?
В соответствии с определением ограждения (никакие чтения/магазины не могут быть перемещены выше/ниже забора), я бы вставлял забор перед чтением, не позволяя чтению переходить через забор и перемещаться назад во времени (ака, будучи кэшируется).
Как предотвращение перемотки чтения вовремя (или последующие инструкции от перемещения назад во времени) гарантируют неизменное (новое) чтение? Как это помогает?
Точно так же я считаю, что волатильная запись должна вводить забор после операции записи, не позволяя процессору перемещать запись вперед во времени (иначе, задерживая запись). Я считаю, что это приведет к тому, что процессор запустит запись в основную память.
Но, к моему удивлению, реализация С# представляет забор перед записью!
[MethodImplAttribute(MethodImplOptions.NoInlining)] // disable optimizations
public static void VolatileWrite(ref int address, int value)
{
MemoryBarrier(); // Call MemoryBarrier to ensure the proper semantic in a portable way.
address = value;
}
Обновление
В соответствии с этим примером, по-видимому, взятым из "С# 4 в двух словах", забор 2, помещенный после того, как запись, как предполагается, заставит запись стираться основная память немедленно, а забор 3, помещенный перед чтением, должен гарантировать свежее чтение:
class Foo{
int _answer;
bool complete;
void A(){
_answer = 123;
Thread.MemoryBarrier(); // Barrier 1
_complete = true;
Thread.MemoryBarrier(); // Barrier 2
}
void B(){
Thread.MemoryBarrier(); // Barrier 3;
if(_complete){
Thread.MemoryBarrier(); // Barrier 4;
Console.WriteLine(_answer);
}
}
}
Идеи в этой книге (и мои собственные личные убеждения), кажется, противоречат идеям реализаций С# VolatileRead
и VolatileWrite
.
Ответы
Ответ 1
Как именно это предотвращает использование операции чтения ранее кэшированное значение?
Это не так. Неустойчивое чтение не гарантирует возврата последнего значения. В простом английском языке все это действительно означает, что следующее чтение вернет более новое значение и не более того.
Как предотвращается перемещение считывания вперед во времени (или последующие инструкции перемещаются назад во времени). неустойчивое (свежее) чтение? Как это помогает?
Будьте осторожны с терминологией здесь. Летучий не синоним свежего. Как я уже упоминал выше, его реальная полезность заключается в том, как два или более неустойчивых чтения соединены вместе. Следующее чтение в последовательности изменчивых чтений будет абсолютно возвращать более новое значение, чем предыдущее чтение того же адреса. Код без блокировки должен быть написан с учетом этой предпосылки. То есть, код должен быть структурирован для работы над принципом работы с более новым значением, а не с последним значением. Вот почему большинство незакрепленных кодов вращается в цикле, пока он не сможет убедиться, что операция полностью успешно.
Идеи в этой книге (и мои собственные личные убеждения) кажутся противоречат идеям С# VolatileRead и VolatileWrite реализации.
Не совсем. Помните volatile!= Свежий. Да, если вы хотите "свежее" чтение, тогда вам нужно поместить забор перед чтением. Но это не то же самое, что делать волатильное чтение. Я говорю, что если реализация VolatileRead
имела вызов Thread.MemoryBarrier
перед инструкцией чтения, то это фактически не приводило бы к изменчивому чтению. Если бы произвело бы свежее чтение, хотя.
Ответ 2
Важно понимать, что volatile
означает не только "не может кэшировать значение", но также дает важные гарантии видимости (точнее, вполне возможно, что волатильная запись, которая поступает только в кеш, зависит исключительно на аппаратное обеспечение и используемые протоколы когерентности кэш-памяти)
Волатильное чтение дает семантику получения, а волатильная запись имеет семантику выпуска. Захват означает, что вы не можете переупорядочить чтение или запись перед забором, в то время как забор забора означает, что вы не можете переместить их после забора. связанный ответ в комментариях объясняет, что на самом деле довольно красиво.
Теперь вопрос в том, что если у нас нет какой-либо барьер памяти перед загрузкой, как бы гарантировано, что мы увидим новейшее значение? Ответ на это: потому что мы также помещаем барьеры памяти после каждой неустойчивой записи, чтобы гарантировать это.
Doug Lea написал замечательное резюме о том, какие барьеры существуют, что они делают и куда их переносить на неустойчивые чтения/записи для JMM в качестве помощи для авторов компиляторов, но текст также весьма полезен для других людей. Волатильные чтения и записи дают одинаковые гарантии как в Java, так и в CLR, что обычно применимо.
Источник - прокрутите вниз до раздела "Блокировки памяти" (я бы скопировал интересные части, но форматирование не сохранилось это..)